Pretty console.log strings feel fine until 2 a.m., when you need every log line for one orderId across three Lambdas, a DLQ hop, and an API Gateway request id. Unstructured text does not survive CloudWatch Logs Insights at scale. AWS Lambda Powertools for TypeScript/Node is the unfair advantage: one Logger with persistent keys, EMF metrics without custom agents, and X-Ray traces that actually annotate business ids — without inventing your own observability framework.
⚡ TL;DR: Adopt
@aws-lambda-powertools/logger,metrics, andtraceras the default handler middleware. Putcold_start,correlation_id, and business keys (tenantId,orderId) on every log. Emit EMF metrics for success/timeout/error; sample traces with annotations you can filter. Prefer JSON log format in CloudWatch. Illustrative bar: p99 Insights queries under a few seconds on a week of logs because every field is keyed — not regex archaeology.
Install once, wrap every handler
npm i @aws-lambda-powertools/logger @aws-lambda-powertools/metrics @aws-lambda-powertools/tracer
# optional middleware helper
npm i @aws-lambda-powertools/commons
// observability.ts — shared across functions in the service
import { Logger } from "@aws-lambda-powertools/logger";
import { Metrics } from "@aws-lambda-powertools/metrics";
import { Tracer } from "@aws-lambda-powertools/tracer";
export const logger = new Logger({
serviceName: "checkout",
logLevel: (process.env.LOG_LEVEL as any) ?? "INFO",
});
export const metrics = new Metrics({
namespace: "CheatCoders/Checkout",
serviceName: "checkout",
});
export const tracer = new Tracer({ serviceName: "checkout" });
// handler.ts
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { logger, metrics, tracer } from "./observability";
import { MetricUnit } from "@aws-lambda-powertools/metrics";
const subsegmentAware = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
const segment = tracer.getSegment();
const sub = segment?.addNewSubsegment(name);
try {
return await fn();
} catch (e) {
sub?.addError(e as Error);
throw e;
} finally {
sub?.close();
}
};
export const handler: APIGatewayProxyHandlerV2 = async (event, context) => {
// ✅ Persist request-scoped keys for all subsequent logs in this invoke
logger.addContext(context);
logger.appendKeys({
correlation_id: event.headers?.["x-correlation-id"] ?? context.awsRequestId,
path: event.rawPath,
});
metrics.captureColdStartMetric(); // Powertools cold-start metric helper when used with Middy / manual pattern
try {
const orderId = event.pathParameters?.orderId;
if (!orderId) {
logger.warn("missing_order_id");
metrics.addMetric("ClientError", MetricUnit.Count, 1);
return { statusCode: 400, body: "bad_request" };
}
logger.appendKeys({ orderId });
tracer.putAnnotation("orderId", orderId);
const result = await subsegmentAware("loadOrder", () => loadOrder(orderId));
metrics.addMetric("OrderLoaded", MetricUnit.Count, 1);
logger.info("order_loaded", { status: result.status });
return { statusCode: 200, body: JSON.stringify(result) };
} catch (err) {
logger.error("handler_failed", { err: err instanceof Error ? err.message : String(err) });
metrics.addMetric("HandlerError", MetricUnit.Count, 1);
throw err;
} finally {
metrics.publishStoredMetrics();
}
};
async function loadOrder(orderId: string) {
return { orderId, status: "PAID" };
}
✅ One correlation id from the edge through every async hop (copy into SQS message attributes).
❌ Log only "error" with no keys — Insights cannot save you.
Structured logs that Insights can chew
Powertools Logger emits JSON. That unlocks queries like:
fields @timestamp, correlation_id, orderId, message, err
| filter service = "checkout"
| filter orderId = "ORD-9F2A"
| sort @timestamp asc
Set log retention and skip debug in prod by default:
// environment
// POWERTOOLS_SERVICE_NAME=checkout
// POWERTOOLS_LOG_LEVEL=INFO
// POWERTOOLS_METRICS_NAMESPACE=CheatCoders/Checkout
// POWERTOOLS_TRACE_ENABLED=true
Redact aggressively — structured does not mean “log the authorization header”:
logger.appendKeys({
// ✅
tenantId: "acme-42",
// ❌ never
// authorization: event.headers.authorization,
});
function scrub(event: Record<string, unknown>) {
const clone = { ...event };
delete (clone as any).password;
delete (clone as any).cardNumber;
return clone;
}
logger.debug("incoming", { event: scrub(event as any) });
For agent/tool Lambdas, the same discipline applies as in Amazon Bedrock Agents: Tool Use, Memory, and Production Guardrails — never put secrets into prompt attributes or log lines.
Metrics without a sidecar: Embedded Metric Format
Powertools Metrics buffers then prints EMF JSON that CloudWatch scrapes automatically. No StatsD daemon.
import { Metrics, MetricUnit } from "@aws-lambda-powertools/metrics";
const metrics = new Metrics({ namespace: "CheatCoders/Checkout", serviceName: "checkout" });
export function recordOutcome(kind: "success" | "timeout" | "error", latencyMs: number) {
metrics.addDimension("Function", process.env.AWS_LAMBDA_FUNCTION_NAME ?? "local");
metrics.addMetric("Invokes", MetricUnit.Count, 1);
metrics.addMetric(
kind === "success" ? "Success" : kind === "timeout" ? "Timeout" : "Error",
MetricUnit.Count,
1
);
metrics.addMetric("LatencyMs", MetricUnit.Milliseconds, latencyMs);
metrics.publishStoredMetrics();
}
Alarm on Timeout and Error separately — see Lambda Timeouts, Retries, and DLQs for why blending them hides SEV-1s. Keep cardinality sane: dimensions like tenantId can explode metric costs; prefer logs for high-cardinality ids and metrics for aggregates.
Tracing: annotations over archaeology
Enable Active Tracing on the function. Powertools Tracer captures AWS SDK v3 clients when you instrument them, and lets you put annotations (indexed) vs metadata (not indexed):
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { tracer } from "./observability";
const raw = new DynamoDBClient({});
export const ddb = tracer.captureAWSv3Client(raw);
export async function markBusiness(orderId: string, amountCents: number) {
tracer.putAnnotation("orderId", orderId); // filterable in X-Ray console
tracer.putMetadata("checkout", { amountCents }); // detail pane only
}
Pass correlation_id as an SQS attribute so the next Lambda’s Logger can appendKeys on consume — that is how you stitch async architectures described in Event-Driven Architecture.
Middy middleware and sampling
If you already use Middy:
import middy from "@middy/core";
import { logger, metrics, tracer } from "./observability";
import { injectLambdaContext } from "@aws-lambda-powertools/logger/middleware";
import { logMetrics } from "@aws-lambda-powertools/metrics/middleware";
import { captureLambdaHandler } from "@aws-lambda-powertools/tracer/middleware";
export const handler = middy(async (event: any) => {
logger.info("ok", { hello: "world" });
return { statusCode: 200, body: "ok" };
})
.use(captureLambdaHandler(tracer))
.use(logMetrics(metrics, { captureColdStartMetric: true }))
.use(injectLambdaContext(logger, { clearState: true }));
clearState: true matters: without it, persistent keys leak across warm invokes (tenant A’s orderId on tenant B’s request). That bug is worse than no Powertools at all.
For high-RPS functions, sample debug logs and traces (X-Ray reservoir + percentage). Keep errors always on.
On-call playbook baked into the code
- Page fires on
Timeoutmetric or DLQ depth. - Open Insights with
correlation_idfrom the alert annotation. - Jump to X-Ray trace via
awsRequestId. - Decide: dependency timeout (budget bug) vs poison message (idempotency / DLQ) vs cold-start eating the budget (Cold Starts on Node 20).
This pairs with Node.js Error Handling: Production Patterns and Monitoring — Powertools is how you make those patterns queryable on Lambda.
Closing checklist
✅ Dos
– ✅ JSON Logger with serviceName, correlation id, business keys
– ✅ clearState / clear keys between invokes when using middleware
– ✅ EMF metrics for success/timeout/error + latency
– ✅ Tracer annotations for ids you filter in incidents
– ✅ Redact tokens/PII; debug sampling in prod
❌ Don’ts
– ❌ Don’t console.log(JSON.stringify(event)) with auth headers
– ❌ Don’t put high-cardinality tenant ids on every metric dimension
– ❌ Don’t forget to publishStoredMetrics
– ❌ Don’t let persistent logger keys bleed across warm requests
– ❌ Don’t run prod at DEBUG “temporarily” for a week
Related reading
- AWS Lambda Best Practices: Write Functions That Scale and Never Time Out
- Node.js Error Handling: Production Patterns and Monitoring
- Amazon Bedrock Agents: Tool Use, Memory, and Production Guardrails
- Event-Driven Architecture: Decoupled Systems With Message Queues
- Lambda Cold Starts on Node 20: Measure, Cut, and Keep Cutting
- Lambda Timeouts, Retries, and DLQs: Idempotent Failure Handling
Last updated on September 10, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: LLM Incident Runbooks: Ground On-Call Answers in CloudWatch Signals - CheatCoders
Pingback: Lambda Test Events as Contracts: Schema-Locked Fixtures Across Teams - CheatCoders