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
| Option | Default | Description |
|---|---|---|
expiresAfterSeconds | 3600 | TTL for cached results. After expiry, the same event triggers fresh execution. |
tableName | IDEMPOTENCY_TABLE_NAME | Override the DynamoDB table name. |
Choosing a TTL
| Handler type | Recommended TTL |
|---|---|
| API handlers | 300–600s (5–10 min) |
| SQS processors | 3600–86400s (1–24 h) |
| Scheduled tasks | Match your schedule interval |
| EventBridge handlers | 3600–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.