← Back to Blog

Serverless Function Caching Patterns

December 22, 2025 • 7 min read • Serverless

Serverless functions have unique caching challenges: cold starts, ephemeral state, and pay-per-invocation pricing. The right caching strategy can cut costs by 80% and eliminate cold start latency. Here's how.

The Serverless Caching Challenge

Traditional caching assumes persistent processes. Serverless breaks this assumption:

You need external caching (Redis, CDN) plus smart connection reuse.

Pattern 1: Connection Reuse

Establish connections outside the handler to reuse across warm invocations:

// AWS Lambda - Reuse database and cache connections

// These persist across warm invocations
let redisClient = null;
let dbPool = null;

async function getRedis() {
    if (!redisClient) {
        const Redis = require('ioredis');
        redisClient = new Redis(process.env.REDIS_URL);
    }
    return redisClient;
}

async function getDB() {
    if (!dbPool) {
        const { Pool } = require('pg');
        dbPool = new Pool({
            connectionString: process.env.DATABASE_URL,
            max: 1  // Single connection per Lambda instance
        });
    }
    return dbPool;
}

exports.handler = async (event) => {
    const cache = await getRedis();
    const db = await getDB();

    // Use cache and db...
};
Cost savings: Connection reuse reduces cold start overhead by 50-200ms and eliminates repeated connection establishment costs.

Pattern 2: Edge Caching (CDN)

Cache responses at the edge for static or semi-static data:

// Vercel Edge Function with caching headers
export const config = { runtime: 'edge' };

export default async function handler(req) {
    const cacheKey = new URL(req.url).pathname;

    // Check if we can serve from cache
    const cached = await fetch(`https://cache.example.com/${cacheKey}`);
    if (cached.ok) {
        return new Response(await cached.text(), {
            headers: {
                'Content-Type': 'application/json',
                'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
                'X-Cache': 'HIT'
            }
        });
    }

    // Generate response
    const data = await generateData();

    // Cache for next time
    await cacheResponse(cacheKey, data);

    return new Response(JSON.stringify(data), {
        headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
            'X-Cache': 'MISS'
        }
    });
}

Pattern 3: Cloudflare Workers KV

Cloudflare's edge key-value store for low-latency caching:

// Cloudflare Worker with KV caching
export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        const cacheKey = `cache:${url.pathname}`;

        // Try KV cache first
        let data = await env.CACHE_KV.get(cacheKey, 'json');

        if (!data) {
            // Fetch from origin
            const response = await fetch(env.API_ORIGIN + url.pathname);
            data = await response.json();

            // Cache in KV (60 second TTL)
            await env.CACHE_KV.put(cacheKey, JSON.stringify(data), {
                expirationTtl: 60
            });
        }

        return new Response(JSON.stringify(data), {
            headers: { 'Content-Type': 'application/json' }
        });
    }
};

Pattern 4: In-Memory Cache for Warm Functions

Cache frequently accessed data in function memory:

// Global cache (survives across warm invocations)
const memoryCache = new Map();
const CACHE_TTL = 60000; // 1 minute

function getCached(key) {
    const cached = memoryCache.get(key);
    if (cached && Date.now() < cached.expiry) {
        return cached.value;
    }
    return null;
}

function setCached(key, value, ttl = CACHE_TTL) {
    memoryCache.set(key, {
        value,
        expiry: Date.now() + ttl
    });

    // Limit cache size
    if (memoryCache.size > 1000) {
        const oldest = memoryCache.keys().next().value;
        memoryCache.delete(oldest);
    }
}

exports.handler = async (event) => {
    const key = `config:${event.tenantId}`;

    // Check memory cache first (fastest)
    let config = getCached(key);

    if (!config) {
        // Check Redis (fast)
        const redis = await getRedis();
        config = await redis.get(key);

        if (!config) {
            // Database (slowest)
            config = await db.query('SELECT * FROM config WHERE tenant_id = $1', [event.tenantId]);
            await redis.set(key, JSON.stringify(config), 'EX', 300);
        }

        setCached(key, config);
    }

    return config;
};

Pattern 5: Pre-warming Critical Paths

Keep functions warm for critical paths:

// CloudWatch scheduled event to keep Lambda warm
// Run every 5 minutes

exports.warmHandler = async (event) => {
    if (event.source === 'aws.events') {
        // This is a warming invocation
        console.log('Warming invocation - keeping connections alive');

        // Touch connections to keep them alive
        const redis = await getRedis();
        await redis.ping();

        return { statusCode: 200, body: 'Warmed' };
    }

    // Regular invocation logic...
};

Cost Optimization

Caching reduces serverless costs in multiple ways:

Serverless caching made simple

Cachee.ai integrates with Lambda, Vercel, and Cloudflare for automatic edge caching with zero config.

Start Free Trial