React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

awslambdaserverlessinvocationcloud-functions

AWS SDK Lambda - Invoke AWS Lambda functions from Next.js with typed payloads and error handling

Recipe

npm install @aws-sdk/client-lambda
// lib/lambda.ts
import { LambdaClient } from "@aws-sdk/client-lambda";
 
export const lambdaClient = new LambdaClient({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});
// lib/invoke-lambda.ts
import { InvokeCommand } from "@aws-sdk/client-lambda";
import { lambdaClient } from "./lambda";
 
export async function invokeLambda<TInput, TOutput>(
  functionName: string,
  payload: TInput
): Promise<TOutput> {
  const command = new InvokeCommand({
    FunctionName: functionName,
    Payload: new TextEncoder().encode(JSON.stringify(payload)),
  });
 
  const response = await lambdaClient.send(command);
 
  if (response.FunctionError) {
    const errorPayload = JSON.parse(
      new TextDecoder().decode(response.Payload)
    );
    throw new Error(
      `Lambda error: ${errorPayload.errorMessage ?? response.FunctionError}`
    );
  }
 
  return JSON.parse(new TextDecoder().decode(response.Payload)) as TOutput;
}

When to reach for this: You need to invoke AWS Lambda functions from a Next.js application for background processing, heavy computation, or accessing AWS-native services.

Working Example

// app/components/ImageProcessor.tsx
"use client";
import { useState } from "react";
import { processImage } from "../actions/image-actions";
 
export default function ImageProcessor() {
  const [result, setResult] = useState<{
    thumbnailUrl: string;
    dimensions: { width: number; height: number };
  } | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
 
  async function handleProcess(formData: FormData) {
    setLoading(true);
    setError(null);
    try {
      const data = await processImage(formData);
      setResult(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Processing failed");
    } finally {
      setLoading(false);
    }
  }
 
  return (
    <div className="max-w-md mx-auto p-6 space-y-4">
      <form action={handleProcess}>
        <input name="imageUrl" placeholder="Image URL" required className="w-full border rounded px-3 py-2" />
        <input name="width" type="number" placeholder="Target width" defaultValue={200} className="w-full border rounded px-3 py-2 mt-2" />
        <button
          type="submit"
          disabled={loading}
          className="mt-3 bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
        >
          {loading ? "Processing..." : "Process Image"}
        </button>
      </form>
      {error && <p className="text-red-600">{error}</p>}
      {result && (
        <div>
          <p>Thumbnail: {result.dimensions.width}x{result.dimensions.height}</p>
          <img src={result.thumbnailUrl} alt="Processed thumbnail" className="border rounded" />
        </div>
      )}
    </div>
  );
}
// app/actions/image-actions.ts
"use server";
import { invokeLambda } from "@/lib/invoke-lambda";
 
interface ProcessImageInput {
  imageUrl: string;
  targetWidth: number;
}
 
interface ProcessImageOutput {
  thumbnailUrl: string;
  dimensions: { width: number; height: number };
}
 
export async function processImage(formData: FormData) {
  const imageUrl = formData.get("imageUrl") as string;
  const targetWidth = Number(formData.get("width")) || 200;
 
  const result = await invokeLambda<ProcessImageInput, ProcessImageOutput>(
    "image-processor",
    { imageUrl, targetWidth }
  );
 
  return result;
}

What this demonstrates:

  • Type-safe Lambda invocation with generic input/output types
  • Server Action wrapping Lambda calls for use in client forms
  • Error handling for Lambda function errors
  • Payload encoding/decoding with TextEncoder/TextDecoder

Deep Dive

How It Works

  • The AWS SDK v3 Lambda client sends HTTP requests to the Lambda API to invoke functions
  • InvokeCommand sends the payload as a Uint8Array and receives the response the same way
  • Synchronous invocation (default) waits for the function to complete and returns the result
  • The FunctionError field on the response indicates if the Lambda function threw an error
  • Payloads are limited to 6MB for synchronous invocations and 256KB for async
  • Lambda functions run in their own runtime (Node.js, Python, etc.) independent of your Next.js app

Variations

Async (fire-and-forget) invocation:

import { InvokeCommand, InvocationType } from "@aws-sdk/client-lambda";
 
const command = new InvokeCommand({
  FunctionName: "email-sender",
  InvocationType: InvocationType.Event, // async, returns 202 immediately
  Payload: new TextEncoder().encode(
    JSON.stringify({ to: "user@example.com", subject: "Welcome!" })
  ),
});
 
await lambdaClient.send(command); // Returns immediately, no response payload

Dry run (validate permissions and input):

const command = new InvokeCommand({
  FunctionName: "my-function",
  InvocationType: InvocationType.DryRun,
  Payload: new TextEncoder().encode(JSON.stringify({ test: true })),
});
 
// Returns 204 if valid, throws if permissions are wrong
await lambdaClient.send(command);

List available functions:

import { ListFunctionsCommand } from "@aws-sdk/client-lambda";
 
const command = new ListFunctionsCommand({ MaxItems: 50 });
const response = await lambdaClient.send(command);
 
const functions = response.Functions?.map((fn) => ({
  name: fn.FunctionName,
  runtime: fn.Runtime,
  memory: fn.MemorySize,
  lastModified: fn.LastModified,
}));

Invoke with qualifier (version or alias):

const command = new InvokeCommand({
  FunctionName: "my-function",
  Qualifier: "production", // alias name or version number
  Payload: new TextEncoder().encode(JSON.stringify(payload)),
});

TypeScript Notes

  • InvokeCommandOutput.Payload is Uint8Array | undefined — always check for undefined before decoding
  • Create typed wrapper functions with generics for each Lambda function you call
  • InvocationType enum provides RequestResponse (sync), Event (async), and DryRun
import type { InvokeCommandOutput } from "@aws-sdk/client-lambda";
 
function decodePayload<T>(response: InvokeCommandOutput): T | null {
  if (!response.Payload) return null;
  return JSON.parse(new TextDecoder().decode(response.Payload)) as T;
}

Gotchas

  • Payload encoding — Passing a plain string or object as Payload throws a type error. Fix: Always encode with new TextEncoder().encode(JSON.stringify(data)) and decode responses with new TextDecoder().decode(response.Payload).

  • Cold start latency — First invocations after idle periods take several seconds. Fix: Use provisioned concurrency for latency-sensitive functions, or accept cold starts and show loading states in the UI.

  • Timeout mismatch — Lambda functions default to 3 seconds, but processing may take longer. Fix: Set the Lambda function timeout appropriately in AWS. Also consider async invocation for long-running tasks.

  • Silent failures with async invocationInvocationType: "Event" returns 202 without error details. Fix: Set up a Dead Letter Queue (DLQ) on the Lambda function to capture failed async invocations.

  • Payload size limit — Synchronous payloads are limited to 6MB. Fix: For larger data, store it in S3 and pass the S3 key in the payload.

  • Missing permissions — The IAM user or role needs lambda:InvokeFunction permission. Fix: Add the permission to your IAM policy for the specific function ARN.

Alternatives

LibraryBest ForTrade-off
@aws-sdk/client-lambdaDirect Lambda invocationRequires AWS setup, IAM permissions
Next.js API RoutesServer-side logic in same appLimited execution time, shared resources
Vercel FunctionsServerless on VercelVercel-specific, less control
AWS API Gateway + LambdaPublic API endpointsMore infrastructure to manage
Step FunctionsOrchestrating multiple LambdasHigher complexity, additional cost

FAQs

Why do I need to encode the payload with TextEncoder when invoking a Lambda?
  • InvokeCommand expects Payload as a Uint8Array, not a plain object or string
  • Use new TextEncoder().encode(JSON.stringify(data)) to convert
  • Responses also come back as Uint8Array and need new TextDecoder().decode()
  • Passing a plain object throws a type error at runtime
What is the difference between synchronous and asynchronous Lambda invocation?
  • Synchronous (default): Waits for the function to finish and returns the result
  • Asynchronous (InvocationType: "Event"): Returns HTTP 202 immediately, no response payload
  • Use synchronous when you need the result right away
  • Use asynchronous for fire-and-forget tasks like sending emails
How do I handle errors from a Lambda function?
const response = await lambdaClient.send(command);
 
if (response.FunctionError) {
  const error = JSON.parse(
    new TextDecoder().decode(response.Payload)
  );
  throw new Error(error.errorMessage);
}

The FunctionError field is set when the Lambda throws; the payload contains error details.

Gotcha: Why does my async Lambda invocation silently fail without any error?
  • InvocationType: "Event" returns 202 without error details even if the function fails
  • Set up a Dead Letter Queue (DLQ) on the Lambda to capture failed invocations
  • Use CloudWatch Logs to debug the function execution
What are the payload size limits for Lambda invocation?
  • Synchronous invocations: 6MB payload limit
  • Asynchronous invocations: 256KB payload limit
  • For larger data, store it in S3 and pass the S3 key in the payload
Gotcha: Why is my Lambda invocation slow on the first call?
  • Lambda functions experience "cold starts" after idle periods
  • The runtime must initialize, load dependencies, and run initialization code
  • Cold starts can take several seconds depending on the runtime and package size
  • Use provisioned concurrency for latency-sensitive functions
How do I create a type-safe Lambda wrapper function in TypeScript?
async function invokeLambda<TInput, TOutput>(
  functionName: string,
  payload: TInput
): Promise<TOutput> {
  const command = new InvokeCommand({
    FunctionName: functionName,
    Payload: new TextEncoder().encode(JSON.stringify(payload)),
  });
  const response = await lambdaClient.send(command);
  return JSON.parse(
    new TextDecoder().decode(response.Payload)
  ) as TOutput;
}
How do I type the InvokeCommandOutput.Payload in TypeScript?
  • Payload is typed as Uint8Array | undefined
  • Always check for undefined before decoding
  • Cast the decoded JSON with as T for your expected return type
  • The SDK does not validate the response shape at compile time
How do I invoke a specific version or alias of a Lambda function?
const command = new InvokeCommand({
  FunctionName: "my-function",
  Qualifier: "production", // alias or version number
  Payload: new TextEncoder().encode(JSON.stringify(payload)),
});
What IAM permissions are required to invoke a Lambda from my Next.js app?
  • The IAM user or role needs lambda:InvokeFunction permission
  • Scope the permission to the specific function ARN for security
  • Use lambda:ListFunctions if you also need to list available functions
How do I wrap a Lambda call in a Next.js Server Action?
"use server";
import { invokeLambda } from "@/lib/invoke-lambda";
 
export async function processImage(formData: FormData) {
  const imageUrl = formData.get("imageUrl") as string;
  return invokeLambda("image-processor", { imageUrl });
}

Server Actions keep AWS credentials on the server and provide a clean API for client components.

What is a DryRun invocation and when would I use it?
  • InvocationType: "DryRun" validates permissions and input without executing the function
  • Returns 204 if the call would succeed, throws if permissions are wrong
  • Useful for testing IAM configuration before making real invocations