or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cache-persistence.mdconfiguration.mddecorator.mddynamodb-persistence.mderrors.mdfunction-wrapper.mdindex.mdmiddleware.mdtypes.md
tile.json

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)