← Back to Blog

Serverless Caching Patterns for AWS Lambda

December 21, 2025 • 7 min read • Serverless Architecture

Serverless architecture promises infinite scale and zero server management, but caching in Lambda environments is fundamentally different from traditional applications. Functions are ephemeral, connections are expensive, and cold starts kill performance. This guide covers proven caching patterns specifically designed for AWS Lambda and serverless architectures.

The Serverless Caching Challenge

Traditional caching assumptions break down in serverless:

Pattern 1: Lambda Execution Context Caching

Lambda reuses execution contexts between invocations. Cache data outside the handler function to persist across invocations:

// Module-level cache (persists across invocations)
let configCache = null;
let cacheTimestamp = 0;
const CACHE_TTL = 300000; // 5 minutes

exports.handler = async (event) => {
  const now = Date.now();

  // Check if cache is valid
  if (!configCache || (now - cacheTimestamp) > CACHE_TTL) {
    console.log('Cache miss - fetching config');
    configCache = await fetchConfig();
    cacheTimestamp = now;
  } else {
    console.log('Cache hit - using cached config');
  }

  // Use cached config
  return processEvent(event, configCache);
};

What to Cache in Execution Context

What NOT to Cache

Pattern 2: ElastiCache for Distributed Caching

For data shared across Lambda invocations, use ElastiCache (Redis/Memcached) with connection pooling:

const Redis = require('ioredis');

// Create Redis client outside handler (reused across invocations)
let redis = null;

function getRedisClient() {
  if (!redis) {
    redis = new Redis({
      host: process.env.REDIS_HOST,
      port: 6379,
      // Connection pooling settings
      maxRetriesPerRequest: 3,
      enableReadyCheck: true,
      // Keep connection alive between invocations
      keepAlive: 30000,
      // Faster connect timeout for Lambda
      connectTimeout: 1000
    });
  }
  return redis;
}

exports.handler = async (event) => {
  const redis = getRedisClient();

  // Try cache first
  const userId = event.userId;
  const cached = await redis.get(`user:${userId}`);

  if (cached) {
    return JSON.parse(cached);
  }

  // Cache miss - fetch and cache
  const user = await fetchUser(userId);
  await redis.setex(`user:${userId}`, 300, JSON.stringify(user));

  return user;
};

ElastiCache Best Practices for Lambda

Pattern 3: DynamoDB as Cache Layer

DynamoDB provides serverless caching with automatic scaling and no connection management:

const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();

async function getCached(key) {
  try {
    const result = await dynamodb.get({
      TableName: 'CacheTable',
      Key: { cacheKey: key }
    }).promise();

    if (result.Item) {
      // Check TTL
      if (result.Item.expiresAt > Date.now()) {
        return result.Item.value;
      }
    }
    return null;
  } catch (err) {
    console.error('Cache error:', err);
    return null;
  }
}

async function setCache(key, value, ttlSeconds) {
  const expiresAt = Date.now() + (ttlSeconds * 1000);

  await dynamodb.put({
    TableName: 'CacheTable',
    Item: {
      cacheKey: key,
      value: value,
      expiresAt: expiresAt
    }
  }).promise();
}

exports.handler = async (event) => {
  const cacheKey = `product:${event.productId}`;

  // Check DynamoDB cache
  let product = await getCached(cacheKey);

  if (!product) {
    // Cache miss
    product = await fetchProduct(event.productId);
    await setCache(cacheKey, product, 600);
  }

  return product;
};

DynamoDB vs ElastiCache

FeatureDynamoDBElastiCache
Latency5-10ms1-2ms
ScalingAutomaticManual cluster sizing
CostPay per requestHourly instance cost
ManagementZeroVPC, security groups
Best forVariable trafficHigh throughput

Pattern 4: CloudFront Edge Caching

Cache Lambda@Edge responses at CloudFront edge locations for global low-latency access:

// Lambda@Edge function
exports.handler = async (event) => {
  const request = event.Records[0].cf.request;

  // Generate cache key from request
  const cacheKey = request.uri + '?' + request.querystring;

  // CloudFront automatically caches based on response headers
  const response = {
    status: '200',
    statusDescription: 'OK',
    headers: {
      'content-type': [{ value: 'application/json' }],
      // Cache for 1 hour at edge
      'cache-control': [{ value: 'public, max-age=3600' }],
      // Allow stale content while revalidating
      'cache-control': [{ value: 's-maxage=3600, stale-while-revalidate=86400' }]
    },
    body: JSON.stringify({ data: 'cached response' })
  };

  return response;
};

When to Use Edge Caching

Pattern 5: API Gateway Caching

Enable API Gateway caching to avoid Lambda invocations for cached responses:

# API Gateway configuration
Resources:
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: CachedAPI

  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      CacheClusterEnabled: true
      CacheClusterSize: '0.5'  # GB
      MethodSettings:
        - ResourcePath: /products/*
          HttpMethod: GET
          CachingEnabled: true
          CacheTtlInSeconds: 300
          CacheDataEncrypted: true

Cache Key Customization

// Lambda function sets cache key parameters
exports.handler = async (event) => {
  const userId = event.requestContext.authorizer.userId;
  const category = event.pathParameters.category;

  // API Gateway caches based on path + query params
  // Add custom cache key via query string
  const products = await getProducts(category, userId);

  return {
    statusCode: 200,
    headers: {
      'Cache-Control': 'max-age=300'  // 5 minutes
    },
    body: JSON.stringify(products)
  };
};

Pattern 6: Warm-Up Strategies

Prevent cold starts by keeping Lambda functions warm:

// CloudWatch Events rule (every 5 minutes)
{
  "schedule": "rate(5 minutes)",
  "input": {
    "warmup": true
  }
}

// Lambda handler
exports.handler = async (event) => {
  // Detect warmup invocation
  if (event.warmup) {
    console.log('Warmup invocation - keeping function warm');
    // Initialize connections, load config
    await initializeConnections();
    return { statusCode: 200, body: 'warmed' };
  }

  // Regular request processing
  return processRequest(event);
};

Pattern 7: Layer-Based Caching

Combine multiple cache layers for optimal performance:

// Layer 1: Execution context cache
// Layer 2: ElastiCache
// Layer 3: Database

let executionCache = new Map();

async function getData(key) {
  // Layer 1: Check execution context
  if (executionCache.has(key)) {
    console.log('L1 cache hit');
    return executionCache.get(key);
  }

  // Layer 2: Check Redis
  const redis = getRedisClient();
  const cached = await redis.get(key);

  if (cached) {
    console.log('L2 cache hit');
    const data = JSON.parse(cached);
    executionCache.set(key, data);  // Promote to L1
    return data;
  }

  // Layer 3: Query database
  console.log('Cache miss - querying database');
  const data = await queryDatabase(key);

  // Populate both cache layers
  executionCache.set(key, data);
  await redis.setex(key, 300, JSON.stringify(data));

  return data;
}

Cold Start Optimization

Reduce Package Size

# Use webpack/esbuild to bundle only required code
# Remove development dependencies

# Before: 50MB package = 3-5s cold start
# After: 5MB package = 500ms-1s cold start

npm install --production
zip -r function.zip . -x "*.git*" "*.test.js" "node_modules/aws-sdk/*"

Lazy Load Dependencies

// Bad: Load everything at module level
const AWS = require('aws-sdk');
const heavyLibrary = require('heavy-library');
const anotherLib = require('another-lib');

// Good: Load only what's needed
exports.handler = async (event) => {
  if (event.operation === 'process') {
    const processor = require('./processor');
    return processor.handle(event);
  }
  // Skip loading processor for other operations
};

Monitoring and Debugging

Track cache effectiveness in Lambda:

const metrics = {
  cacheHits: 0,
  cacheMisses: 0,
  executionContextReused: false
};

exports.handler = async (event) => {
  // Detect if execution context was reused
  if (metrics.cacheHits > 0 || metrics.cacheMisses > 0) {
    metrics.executionContextReused = true;
  }

  // Track cache metrics
  const cached = await getFromCache(event.key);

  if (cached) {
    metrics.cacheHits++;
  } else {
    metrics.cacheMisses++;
  }

  // Log metrics
  console.log(JSON.stringify({
    cacheHits: metrics.cacheHits,
    cacheMisses: metrics.cacheMisses,
    hitRate: metrics.cacheHits / (metrics.cacheHits + metrics.cacheMisses),
    contextReused: metrics.executionContextReused
  }));
};

Cost Optimization

Choose the right caching strategy based on traffic patterns:

Conclusion

Caching in serverless environments requires rethinking traditional approaches. Use execution context caching for configuration, ElastiCache or DynamoDB for distributed state, API Gateway/CloudFront for edge caching, and multi-layer strategies for optimal performance. The key is understanding Lambda's ephemeral nature and designing cache strategies that work with it, not against it.

Start with execution context caching for quick wins, add distributed caching as traffic grows, and implement edge caching for global applications.

Serverless Caching Made Simple

Cachee.ai integrates seamlessly with AWS Lambda, handling distributed caching and connection management automatically.

Start Free Trial