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 @mantleframework/resilience.

Infrastructure

Add the DynamoDB table in Terraform:

hcl
module "idempotency" {
  source      = "../../../mantle/modules/idempotency"
  name_prefix = module.core.name_prefix
  tags        = module.core.common_tags
}

module "process_order" {
  source        = "../../../mantle/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 '@mantleframework/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 '@mantleframework/resilience'
import { getDrizzleClient } from '@mantleframework/database'
import { getRequiredEnv } from '@mantleframework/env'
import { logger } from '@mantleframework/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.