This guide covers Lambda timeouts retries with production-focused examples and trade-offs.
The ugliest Lambda incidents are not “function timed out.” They are timeouts plus retries plus side effects: a payment captured twice, a fulfillment message published three times, a DLQ nobody owns. Node handlers thatawait a 29s HTTP call inside a 30s function timeout are a coin flip; async event sources that retry forever without idempotency keys are a slow-motion outage. This is the production shape: budgets at every hop, retries that are safe by construction, and DLQs with a replay runbook.
⚡ TL;DR: Set Lambda timeout to work budget + jitter cushion, not 900s “for safety.” Cap downstream HTTP/SDK timeouts below the function timeout. For SQS/SNS/EventBridge/Streams, understand the retry contract and make handlers idempotent with DynamoDB conditional writes or idempotency keys (Powertools). Wire DLQs (or failed-event destinations) and alert on depth — a silent DLQ is a deferred SEV-1. Illustrative targets: API handlers 3–10s; queue workers 30–60s with partial batch failure; poison messages isolated within minutes, not days.
Timeout budgets: function clock vs dependency clock — Lambda timeouts retries
Lambda kills the isolate when the configured timeout hits. Your AWS SDK client may still be waiting unless you set its timeout lower. Two clocks, one process.// handler.ts — explicit budgets beat hope
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { NodeHttpHandler } from "@smithy/node-http-handler";
// Function timeout in console/Terraform: 8s for this API path (illustrative)
const ddb = new DynamoDBClient({
requestHandler: new NodeHttpHandler({
connectionTimeout: 1_000,
requestTimeout: 2_500, // ✅ well under 8s function timeout
}),
maxAttempts: 2, // ✅ finite; don’t stack SDK retries × async retries blindly
});
export async function handler(event: { orderId: string }) {
const ac = new AbortController();
const kill = setTimeout(() => ac.abort(), 6_000); // leave ~2s for cleanup/logging
try {
// pass ac.signal into fetch / ORM calls
const res = await fetch(process.env.PRICING_URL! + "/" + event.orderId, {
signal: ac.signal,
});
if (!res.ok) throw new Error(`pricing_${res.status}`);
return { ok: true };
} finally {
clearTimeout(kill);
}
}
❌ Function timeout 900s with default SDK retries — you will pay for hung sockets and duplicate work. For API Gateway + Lambda, remember the integration timeout ceiling (historically ~29s for REST; check current HTTP API limits). A 60s Lambda behind API Gateway does not give you a 60s client experience.
Retries differ by event source — learn the contract
| Source | Typical retry behavior | What you must design |
|---|---|---|
| Synchronous (API GW, SDK invoke) | Caller retries | Idempotency on mutating verbs |
| SQS | Until retention / redrive | Partial batch failure + DLQ |
| SNS → Lambda | Async retry + destination | OnFailure destination / DLQ |
| EventBridge | Retry policy on target | DLQ on rule target |
| DynamoDB / Kinesis streams | Bisect / block shard | Bisect poison; don’t infinite loop |
// SQS partial batch failure — Node 20 / AWS Lambda
import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
export async function handler(event: SQSEvent): Promise<SQSBatchResponse> {
const batchItemFailures: { itemIdentifier: string }[] = [];
await Promise.all(
event.Records.map(async (record) => {
try {
await processOnce(record.body, record.messageId);
} catch (err) {
console.error("record_failed", { id: record.messageId, err: String(err) });
// ✅ only failing ids retry; successes delete
batchItemFailures.push({ itemIdentifier: record.messageId });
}
})
);
return { batchItemFailures };
}
// ❌ throw on first failure with ReportBatchItemFailures off → whole batch retries → dupes
FunctionResponseTypes = ReportBatchItemFailures on the event source mapping. Pair with a redrive policy:
resource "aws_sqs_queue" "orders" {
name = "orders"
visibility_timeout_seconds = 60 # ≥ 6× Lambda timeout is a common rule of thumb
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.orders_dlq.arn
maxReceiveCount = 3
})
}
resource "aws_sqs_queue" "orders_dlq" {
name = "orders-dlq"
}
Idempotency: the only safe retry story
Retries are mandatory in distributed systems. Side effects must tolerate them.// idempotency with DynamoDB conditional put — unfair-advantage minimal version
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
PutCommand,
GetCommand,
} from "@aws-sdk/lib-dynamodb";
const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.IDEMPOTENCY_TABLE!;
export async function processOnce(body: string, messageId: string) {
const payload = JSON.parse(body) as { orderId: string; amountCents: number };
const pk = `ORDER#${payload.orderId}`;
try {
await doc.send(
new PutCommand({
TableName: TABLE,
Item: {
pk,
status: "IN_FLIGHT",
messageId,
ttl: Math.floor(Date.now() / 1000) + 86400,
},
ConditionExpression: "attribute_not_exists(pk) OR #s = :failed",
ExpressionAttributeNames: { "#s": "status" },
ExpressionAttributeValues: { ":failed": "FAILED" },
})
);
} catch (e: any) {
if (e?.name === "ConditionalCheckFailedException") {
const existing = await doc.send(new GetCommand({ TableName: TABLE, Key: { pk } }));
if (existing.Item?.status === "COMPLETED") {
// ✅ safe replay
return existing.Item.result;
}
throw e; // still in flight — let SQS retry with backoff
}
throw e;
}
const result = await chargeCard(payload); // side effect
await doc.send(
new PutCommand({
TableName: TABLE,
Item: { pk, status: "COMPLETED", result, messageId, ttl: Math.floor(Date.now() / 1000) + 86400 },
})
);
return result;
}
@aws-lambda-powertools/idempotency) with a DynamoDB persistence store — same idea, less glue. Key on a business id (orderId), not only messageId, so SNS redeliveries and client retries collapse correctly.
✅ Idempotency key = business intent.❌ “It’s OK, SQS is exactly-once” — it is not.
DLQs and failure destinations you actually operate
A DLQ without an alarm is a trash folder. A DLQ without a replay path is how you lose orders forever.// CloudWatch alarm sketch (conceptual)
// ApproximateNumberOfMessagesVisible > 0 for 5 minutes on orders-dlq → page
// Replay tool — intentional, rate-limited, idempotent handler still required
import { SQSClient, ReceiveMessageCommand, SendMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";
const sqs = new SQSClient({});
export async function replayDlq(dlqUrl: string, mainUrl: string, max = 10) {
const got = await sqs.send(new ReceiveMessageCommand({
QueueUrl: dlqUrl,
MaxNumberOfMessages: max,
VisibilityTimeout: 30,
}));
for (const m of got.Messages ?? []) {
await sqs.send(new SendMessageCommand({ QueueUrl: mainUrl, MessageBody: m.Body! }));
await sqs.send(new DeleteMessageCommand({ QueueUrl: dlqUrl, ReceiptHandle: m.ReceiptHandle! }));
}
}
aws lambda put-function-event-invoke-config \
--function-name fulfill-order \
--maximum-retry-attempts 2 \
--destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:REGION:ACCT:fulfill-dlq"}}'
Observability: timeout vs error vs throttle
Emit explicit outcomes so dashboards don’t lie:import { Logger } from "@aws-lambda-powertools/logger";
import { Metrics, MetricUnit } from "@aws-lambda-powertools/metrics";
const logger = new Logger({ serviceName: "orders" });
const metrics = new Metrics({ namespace: "CheatCoders/Orders", serviceName: "orders" });
export async function guarded<T>(name: string, fn: () => Promise<T>): Promise<T> {
const start = Date.now();
try {
const out = await fn();
metrics.addMetric(`${name}Success`, MetricUnit.Count, 1);
return out;
} catch (err: any) {
const timedOut = err?.name === "AbortError" || /Timeout/i.test(String(err));
metrics.addMetric(timedOut ? `${name}Timeout` : `${name}Error`, MetricUnit.Count, 1);
logger.error("downstream_failed", { name, timedOut, err: String(err), ms: Date.now() - start });
throw err;
} finally {
metrics.publishStoredMetrics();
}
}
Closing checklist
✅ Dos – ✅ Set SDK/HTTP timeouts below the function timeout; leave cushion for logs – ✅ Enable partial batch failure on SQS; tune visibility ≥ Lambda timeout – ✅ Idempotency keys on every mutating consumer – ✅ DLQ + alarm + documented replay – ✅ Metric timeout vs error vs throttle separately ❌ Don’ts – ❌ Don’t default to 900s timeouts to “stop the noise” – ❌ Don’t stack infinite SDK retries on top of SQS retries – ❌ Don’t treat messageId alone as business idempotency – ❌ Don’t ship a DLQ without an owner and a page – ❌ Don’t replay DLQs into non-idempotent handlersRelated reading
- AWS Lambda Best Practices: Write Functions That Scale and Never Time Out
- Event-Driven Architecture: Decoupled Systems With Message Queues
- Node.js Error Handling: Production Patterns and Monitoring
- AWS DynamoDB: Advanced Patterns for Production at Scale
- Lambda Cold Starts on Node 20: Measure, Cut, and Keep Cutting
- Lambda Powertools for Node: Structured Logs That Survive On-Call
Related: Lambda Destinations: Async Failure Routing Beyond Plain SQS DLQs · Lambda Powertools Idempotency: DynamoDB Keys That Survive Retries · Bedrock Converse toolConfig: Idempotent Tool Results Under Retries
Related: Saga Compensations Under Partial Failure: Idempotent Undo Paths That Run · Lambda Response Streaming: Keep AI Coding Gateways Under Client Timeouts · Node.js Error Handling: From try/catch to Production Error Monitoring
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Lambda Powertools for Node: Structured Logs That Survive On-Call - CheatCoders
Pingback: Bedrock Agents: Idempotent Tool Calls Against DynamoDB Writes - CheatCoders
Pingback: Lambda Recursive Loop Detection: Break Accidental Invoke Storms Early - CheatCoders