Lambda Powertools Idempotency: DynamoDB Keys That Survive Retries

Lambda Powertools Idempotency: DynamoDB Keys That Survive Retries

Retries are not edge cases — they are the product. API Gateway clients time out and retry, EventBridge at-least-once delivers twice, Step Functions redrive, and your deploy swaps code mid-flight. Powertools idempotency is only an unfair advantage when the DynamoDB key is stable across those realities and the persistence window outlives the retry storm.

⚡ TL;DR: Hash a business idempotency key (not the whole event). Scope by function alias/version when payloads are version-sensitive. Set expiry past your max client retry + DLQ redrive window. Use IN_PROGRESS locking so concurrent duplicates wait instead of double-charging. Pair with Bedrock Agents: Idempotent Tool Calls and Lambda Warm Pools.

Key from business identity, not raw event JSON

import { makeIdempotent, IdempotencyConfig } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";

const persistence = new DynamoDBPersistenceLayer({ tableName: process.env.IDEMP_TABLE! });

// GOOD: stable across redelivery jitter (new eventId, same orderId)
const config = new IdempotencyConfig({
  eventKeyJmesPath: "detail.orderId",
  expiresAfterSeconds: 60 * 60 * 24, // 24h > client retry + DLQ
  useLocalCache: true,
  throwOnNoIdempotencyKey: true,
});

async function charge(event: { detail: { orderId: string; amount: number } }) {
  return { charged: event.detail.amount, orderId: event.detail.orderId };
}

export const handler = makeIdempotent(charge, { persistenceStore: persistence, config });
// BAD: whole-event hash — EventBridge envelope changes → duplicate charge
const badConfig = new IdempotencyConfig({
  eventKeyJmesPath: "@", // unique every delivery
  expiresAfterSeconds: 30, // shorter than client retry budget
});
void badConfig;

✅ JMESPath to orderId / Idempotency-Key header.
❌ Hashing timestamps, eventId, or request context that changes on retry.

API Gateway header keys that clients can control

import type { APIGatewayProxyEventV2 } from "aws-lambda";

const httpConfig = new IdempotencyConfig({
  eventKeyJmesPath: 'headers."idempotency-key"',
  expiresAfterSeconds: 60 * 15,
  throwOnNoIdempotencyKey: true,
});

export const apiHandler = makeIdempotent(
  async (event: APIGatewayProxyEventV2) => {
    const body = JSON.parse(event.body ?? "{}");
    return { statusCode: 200, body: JSON.stringify(await createPayment(body)) };
  },
  { persistenceStore: persistence, config: httpConfig },
);

Reject missing keys with 400 at the edge. Document that retries must reuse the same Idempotency-Key.

Survive deploys and concurrent duplicates

const deploySafe = new IdempotencyConfig({
  eventKeyJmesPath: "detail.orderId",
  // Include payload hash when handler semantics change across versions
  payloadValidationJmesPath: "detail",
  expiresAfterSeconds: 86_400,
});

During blue/green, payloadValidationJmesPath prevents returning a cached response from an old code path when the body shape changed. Concurrent invokes: Powertools writes IN_PROGRESS; second caller either waits or gets ThereIsAlreadyAExecutionInProgress — handle that as 409/retry, never as “charge again.”

DynamoDB table shape that will not surprise you

// PK: id (idempotency key), attrs: expiration, status, data_hash, response_data
// Enable TTL on expiration attribute. Provision enough WCU for peak retry storms.

Watch ConditionalCheckFailed and latency on the idempotency table during incidents — a hot partition on a popular orderId prefix needs random suffixing only if you control both writer and key design (usually you should not shard the business key).

Closing checklist

  • [ ] Idempotency key = business id or client Idempotency-Key, not full event
  • [ ] TTL/expiry > max client retry + DLQ/redrive window
  • [ ] throwOnNoIdempotencyKey for mutating APIs
  • [ ] Payload validation enabled across versioned deploys when needed
  • [ ] Table TTL on; alarms on ConditionalCheckFailed and throttle
  • [ ] Concurrent in-progress treated as retry/409, never double side-effect

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply