Idempotency utility for AWS Lambda functions that prevents duplicate executions by tracking request payloads in DynamoDB or cache stores, with support for function wrappers, decorators, and Middy middleware.
The CachePersistenceLayer class provides Redis and Valkey-backed storage for idempotency records. It uses cache clients compatible with the CacheClient interface for high-performance scenarios.
Stores idempotency records in Redis or Valkey cache stores with automatic TTL and orphan record handling.
/**
* Valkey and Redis OSS-compatible persistence layer for idempotency records.
*
* Uses a cache client to write and read idempotency records.
* Supports any client that implements the CacheClient interface.
* You must provide your own connected client instance.
*
* @param options - Configuration options for cache persistence
*/
class CachePersistenceLayer extends BasePersistenceLayer {
constructor(options: CachePersistenceOptions);
}
interface CachePersistenceOptions extends BasePersistenceAttributes {
/** Connected cache client instance (required) */
client: CacheClient;
/** Status attribute name (default: 'status') */
statusAttr?: string;
/** Expiry timestamp attribute name (default: 'expiration') */
expiryAttr?: string;
/** In-progress expiry timestamp attribute name (default: 'in_progress_expiration') */
inProgressExpiryAttr?: string;
/** Response data attribute name (default: 'data') */
dataAttr?: string;
/** Payload validation hash attribute name (default: 'validation') */
validationKeyAttr?: string;
}
/**
* Interface for clients compatible with Valkey and Redis-OSS operations.
* Defines the minimum set of operations for cache persistence.
*/
interface CacheClient {
/**
* Retrieves the value associated with the given key
* @param name - The key to get the value for
* @returns The value or null if not found
*/
get(name: string): Promise<CacheValue | null>;
/**
* Sets the value for the specified key with optional parameters
* @param name - The key to set
* @param value - The value to set
* @param options - Optional parameters (e.g., { EX: ttl, NX: true })
* @returns The value set or null if operation failed
*/
set(
name: CacheValue,
value: unknown,
options?: unknown
): Promise<CacheValue | null>;
/**
* Deletes the specified keys from the cache
* @param keys - The keys to delete
* @returns Number of keys deleted
*/
del(keys: string[]): Promise<number>;
}
type CacheValue = string | Uint8Array<ArrayBufferLike>;Usage Examples:
Using Valkey Glide Client
import { GlideClient } from '@valkey/valkey-glide';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
// Create and configure Valkey Glide client
const client = await GlideClient.createClient({
addresses: [{
host: String(process.env.CACHE_ENDPOINT),
port: Number(process.env.CACHE_PORT),
}],
useTLS: true,
requestTimeout: 2000,
});
const persistenceStore = new CachePersistenceLayer({
client,
});
const myHandler = async (event: any, context: any) => {
// Your logic
};
export const handler = makeIdempotent(myHandler, { persistenceStore });Using Redis Client
import { createClient } from '@redis/client';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
// Create and connect Redis client
const redisClient = await createClient({
url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
username: 'default',
}).connect();
const persistenceStore = new CachePersistenceLayer({
client: redisClient,
});
const myHandler = async (event: any, context: any) => {
// Your logic
};
export const handler = makeIdempotent(myHandler, { persistenceStore });With Custom Attribute Names
import { createClient } from '@redis/client';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
const redisClient = await createClient({
url: process.env.REDIS_URL,
}).connect();
const persistenceStore = new CachePersistenceLayer({
client: redisClient,
statusAttr: 'state',
expiryAttr: 'ttl',
dataAttr: 'response',
validationKeyAttr: 'hash',
});
const myHandler = async (event: any, context: any) => {
// Your logic
};
export const handler = makeIdempotent(myHandler, { persistenceStore });With ElastiCache Redis
import { createClient } from '@redis/client';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeIdempotent, IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
// Connect to AWS ElastiCache Redis
const redisClient = await createClient({
socket: {
host: process.env.ELASTICACHE_ENDPOINT,
port: 6379,
},
}).connect();
const persistenceStore = new CachePersistenceLayer({
client: redisClient,
});
const config = new IdempotencyConfig({
expiresAfterSeconds: 600, // 10 minutes
});
const myHandler = async (event: any, context: any) => {
// Your logic
};
export const handler = makeIdempotent(myHandler, { persistenceStore, config });With MemoryDB for Redis
import { createClient } from '@redis/client';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
// Connect to AWS MemoryDB with TLS
const redisClient = await createClient({
socket: {
host: process.env.MEMORYDB_ENDPOINT,
port: 6379,
tls: true,
},
username: 'default',
password: process.env.MEMORYDB_PASSWORD,
}).connect();
const persistenceStore = new CachePersistenceLayer({
client: redisClient,
});
const myHandler = async (event: any, context: any) => {
// Your logic
};
export const handler = makeIdempotent(myHandler, { persistenceStore });Connection Reuse Pattern
import { createClient } from '@redis/client';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
// Create client outside handler for connection reuse across invocations
let redisClient: ReturnType<typeof createClient> | null = null;
const getRedisClient = async () => {
if (!redisClient) {
redisClient = createClient({
url: process.env.REDIS_URL,
});
await redisClient.connect();
}
return redisClient;
};
const myHandler = async (event: any, context: any) => {
// Your logic
};
// Initialize at module load time
const client = await getRedisClient();
const persistenceStore = new CachePersistenceLayer({ client });
export const handler = makeIdempotent(myHandler, { persistenceStore });Records are stored as JSON strings with the idempotency key as the cache key:
{
"status": "COMPLETED",
"expiration": 1234567890,
"in_progress_expiration": 1234567890000,
"data": { "statusCode": 200, "body": "..." },
"validation": "e5f6g7h8..."
}Cache Key Format: {keyPrefix}#{hash} where:
keyPrefix defaults to Lambda function namehash is generated from the payload using the configured hash functionThe cache persistence layer automatically sets TTL (Time To Live) on all records:
EX option on SET commandsexpiresAfterSeconds configurationThe persistence layer handles orphaned records (from timed-out Lambda executions):
in_progress_expiration timestampsIdempotencyItemAlreadyExistsError if lock failsThe cache persistence layer uses conditional SET operations:
NX flag to set only if key doesn't existIdempotencyItemAlreadyExistsErrorIdempotencyItemAlreadyExistsError if not expiredYour cache client must implement:
get(key) - Retrieve value by keyset(key, value, options) - Set value with options (must support EX and NX)del(keys) - Delete one or more keysIdempotencyItemNotFoundError - Record not found during retrievalIdempotencyItemAlreadyExistsError - Duplicate request detected or lock acquisition failedIdempotencyPersistenceConsistencyError - JSON parsing error or orphaned record detectedIdempotencyUnknownError - Invalid operation (e.g., trying to insert non-INPROGRESS record)Install with Tessl CLI
npx tessl i tessl/npm-aws-lambda-powertools--idempotency