CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-aws-lambda-powertools--idempotency

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.

Overview
Eval results
Files

cache-persistence.mddocs/

Cache Persistence

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.

Capabilities

Cache Persistence Layer Class

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 });

Cache Record Structure

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 name
  • hash is generated from the payload using the configured hash function

Automatic TTL Management

The cache persistence layer automatically sets TTL (Time To Live) on all records:

  • Uses Redis/Valkey EX option on SET commands
  • TTL is calculated based on expiresAfterSeconds configuration
  • Records are automatically removed when TTL expires

Orphan Record Handling

The persistence layer handles orphaned records (from timed-out Lambda executions):

  1. Detection: Identifies records with expired in_progress_expiration timestamps
  2. Lock Acquisition: Acquires a 10-second lock to prevent race conditions
  3. Cleanup: Overwrites the orphaned record with the new request
  4. Error on Conflict: Throws IdempotencyItemAlreadyExistsError if lock fails

Conditional Write Logic

The cache persistence layer uses conditional SET operations:

  1. New Record: Uses NX flag to set only if key doesn't exist
  2. Active COMPLETED Record: Throws IdempotencyItemAlreadyExistsError
  3. Active INPROGRESS Record: Throws IdempotencyItemAlreadyExistsError if not expired
  4. Orphaned Record: Acquires lock and overwrites after timeout

Supported Cache Stores

  • Valkey: Open-source Redis alternative (recommended for new projects)
  • Redis OSS: Community edition Redis
  • AWS ElastiCache for Redis: Managed Redis service
  • AWS MemoryDB for Redis: Redis-compatible durable database
  • Any cache implementing CacheClient interface

Client Requirements

Your cache client must implement:

  • get(key) - Retrieve value by key
  • set(key, value, options) - Set value with options (must support EX and NX)
  • del(keys) - Delete one or more keys

Performance Considerations

  • Latency: Cache stores typically have lower latency than DynamoDB
  • Connection Pooling: Reuse client connections across Lambda invocations
  • Network: Deploy Lambda and cache in the same VPC for best performance
  • Throughput: Cache stores can handle very high request rates
  • Cost: Generally lower cost per operation than DynamoDB

Error Handling

  • IdempotencyItemNotFoundError - Record not found during retrieval
  • IdempotencyItemAlreadyExistsError - Duplicate request detected or lock acquisition failed
  • IdempotencyPersistenceConsistencyError - JSON parsing error or orphaned record detected
  • IdempotencyUnknownError - Invalid operation (e.g., trying to insert non-INPROGRESS record)

Install with Tessl CLI

npx tessl i tessl/npm-aws-lambda-powertools--idempotency

docs

cache-persistence.md

configuration.md

decorator.md

dynamodb-persistence.md

errors.md

function-wrapper.md

index.md

middleware.md

types.md

tile.json