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
InvokeCommandsends the payload as aUint8Arrayand receives the response the same way- Synchronous invocation (default) waits for the function to complete and returns the result
- The
FunctionErrorfield 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 payloadDry 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.PayloadisUint8Array | undefined— always check for undefined before decoding- Create typed wrapper functions with generics for each Lambda function you call
InvocationTypeenum providesRequestResponse(sync),Event(async), andDryRun
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
Payloadthrows a type error. Fix: Always encode withnew TextEncoder().encode(JSON.stringify(data))and decode responses withnew 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 invocation —
InvocationType: "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:InvokeFunctionpermission. Fix: Add the permission to your IAM policy for the specific function ARN.
Alternatives
| Library | Best For | Trade-off |
|---|---|---|
| @aws-sdk/client-lambda | Direct Lambda invocation | Requires AWS setup, IAM permissions |
| Next.js API Routes | Server-side logic in same app | Limited execution time, shared resources |
| Vercel Functions | Serverless on Vercel | Vercel-specific, less control |
| AWS API Gateway + Lambda | Public API endpoints | More infrastructure to manage |
| Step Functions | Orchestrating multiple Lambdas | Higher complexity, additional cost |
FAQs
Why do I need to encode the payload with TextEncoder when invoking a Lambda?
InvokeCommandexpectsPayloadas aUint8Array, not a plain object or string- Use
new TextEncoder().encode(JSON.stringify(data))to convert - Responses also come back as
Uint8Arrayand neednew 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?
Payloadis typed asUint8Array | undefined- Always check for
undefinedbefore decoding - Cast the decoded JSON with
as Tfor 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:InvokeFunctionpermission - Scope the permission to the specific function ARN for security
- Use
lambda:ListFunctionsif 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
Related
- AWS SDK S3 — Pass S3 keys to Lambda for file processing
- AWS SDK Polly — Another AWS service integration
- Next.js Server Actions — Wrapping Lambda calls in actions