Skip to content

Idempotency ​

Lambda functions can be retried by API Gateway, SQS, and EventBridge — idempotency ensures duplicate invocations produce the same result without side effects.

Mantle wraps AWS Lambda Powertools Idempotency via @j0nathan-ll0yd/resilience.

Infrastructure ​

Add the DynamoDB table in Terraform:

hcl
module "idempotency" {
  source      = "../node_modules/@j0nathan-ll0yd/cli/modules/idempotency"
  name_prefix = module.core.name_prefix
  tags        = module.core.common_tags
}

module "process_order" {
  source        = "../node_modules/@j0nathan-ll0yd/cli/modules/lambda"
  name_prefix   = module.core.name_prefix
  function_name = "ProcessOrder"
  # ...
  environment_variables = {
    IDEMPOTENCY_TABLE_NAME = module.idempotency.table_name
  }
}

The module creates ${name_prefix}-Idempotency with TTL on expiration, PAY_PER_REQUEST billing.

Basic Usage ​

typescript
import {
  createIdempotencyStore,
  createIdempotencyConfig,
  makeIdempotent,
} from '@j0nathan-ll0yd/resilience'
import type { SQSEvent, Context } from 'aws-lambda'

const store = createIdempotencyStore()          // reads IDEMPOTENCY_TABLE_NAME
const config = createIdempotencyConfig()        // default TTL: 3600s

const processOrder = async (event: SQSEvent, context: Context) => {
  config.registerLambdaContext(context)         // enables timeout tracking
  // your logic — runs once per unique event
  return { success: true }
}

export const handler = makeIdempotent(processOrder, {
  persistenceStore: store,
  config,
})

makeIdempotent caches the return value in DynamoDB. Duplicate invocations return the cached result immediately.

Full Example: Idempotent SQS Handler ​

typescript
import {
  createIdempotencyStore,
  createIdempotencyConfig,
  makeIdempotent,
} from '@j0nathan-ll0yd/resilience'
import { getDrizzleClient } from '@j0nathan-ll0yd/database'
import { getRequiredEnv } from '@j0nathan-ll0yd/env'
import { logger } from '@j0nathan-ll0yd/observability'
import type { SQSEvent, Context } from 'aws-lambda'
import { orders } from '../entities/schema'

const store = createIdempotencyStore()
const config = createIdempotencyConfig({ expiresAfterSeconds: 3600 })

const processOrder = async (event: SQSEvent, context: Context) => {
  config.registerLambdaContext(context)

  for (const record of event.Records) {
    const order = JSON.parse(record.body)
    logger.info('Processing order', { orderId: order.id })

    const db = await getDrizzleClient({
      provider: 'aurora-dsql',
      hostname: getRequiredEnv('DSQL_ENDPOINT'),
      region: getRequiredEnv('AWS_REGION'),
    })

    await db.insert(orders).values({ id: order.id, status: 'completed' })
  }

  return { success: true }
}

export const handler = makeIdempotent(processOrder, {
  persistenceStore: store,
  config,
})

Configuration ​

OptionDefaultDescription
expiresAfterSeconds3600TTL for cached results. After expiry, the same event triggers fresh execution.
tableNameIDEMPOTENCY_TABLE_NAMEOverride the DynamoDB table name.

Choosing a TTL ​

Handler typeRecommended TTL
API handlers300–600s (5–10 min)
SQS processors3600–86400s (1–24 h)
Scheduled tasksMatch your schedule interval
EventBridge handlers3600–21600s (1–6 h)

How Keys Are Generated ​

Powertools hashes the full event payload with SHA-256. For SQS, each message's unique messageId ensures per-message deduplication even in batches.