Database Caching Layers

Production engineering guide for database caching layers covering patterns, implementation strategies, and operational best practices.

Database caching layers sit between an application and its database to reduce read latency, lower query volume, and smooth traffic spikes. They matter whenever read traffic exceeds what a database can sustain with acceptable latency, or when individual query latency must stay below a strict threshold. Caching operates at multiple levels: the database engine’s internal buffer pool, an adjacent external cache (Redis, Memcached, etc.), or a dedicated caching service embedded in the application stack. Each layer has distinct trade-offs around coherency, latency, and operational complexity.

The orientation here is not about whether to cache—it is about which layer to use, how to keep it consistent, and what breaks when it goes wrong.

Choosing a Caching Strategy

Cache-aside (Lazy Loading)

Cache-aside is the most common pattern. On a read, the application checks the cache; if the key exists (a hit), it returns the value. If not (a miss), it fetches from the database, populates the cache, and returns the value. On a write, the application updates the database first, then invalidates or updates the cache entry.

Real command example (Redis):

# Check cache
redis-cli GET user:123

# Cache miss: fetch from PostgreSQL, populate cache with 300s TTL
psql -c "SELECT * FROM users WHERE id = 123" | redis-cli SET user:123 EX 300

Real command example (Memcached):

get user:123
# miss
fetch_from_db | memcached set user:123 0 300 <bytes>

The critical detail: the application must handle the race between a stale cache entry and a concurrent write. If two requests miss the cache simultaneously, both will hit the database and repopulate the cache with the same value—inefficient but not incorrect.

Write-Through

Write-through caches update the database and the cache atomically. The application writes to the cache, the cache propagates the write to the database, and the application receives an acknowledgment. This eliminates stale reads but adds write latency, which depends on the database round-trip.

No standard CLI ships with a turnkey write-through cache, but the pattern maps to Redis with Lua scripting or to commercial caching services that offer write-through modes. The configuration key is typically something like write-through=true or cache-storage=write_through, but exact syntax varies by product and must be verified against documentation.

Write-Behind (Write-Back)

Write-behind defers database writes, buffering them in the cache until a flush trigger (periodic batch, memory pressure, or explicit commit). This gives the lowest write latency but risks data loss if the cache process crashes before flushing.

A real-world pattern: Redis with appendonly no and a separate persistence process, or a dedicated cache product with configurable write-behind intervals. Again, the mechanism is product-specific; the generic pattern is “buffer writes in the cache layer, flush to database on trigger.”

Cache Invalidation and Coherency

Invalidation is where most production incidents originate. Three primary patterns exist:

  1. Time-based TTL – Entries expire after a fixed duration. Simple, but entries may live longer than intended if traffic patterns shift, or evict prematurely under memory pressure.
  1. Event-driven invalidation – A write operation publishes an invalidation message (often via a message queue) that reaches the cache layer, which removes or updates the key. This requires reliable messaging and idempotent invalidation logic.
  1. Version/stamp-based invalidation – The cache stores a version number alongside the value. On every write, the version increments. A read fetches both value and version; if the version in the cache differs from the database’s current version, the cache is stale.

Error text you will see:

redis.exceptions.RedisError: WRONGTYPE Operation against a key holding the wrong kind of value

This occurs when a key was originally stored as a string, then a hash or list was pushed to it, and a GET command is issued. The error is harmless but indicates a schema drift that should be addressed.

memcached ERROR: invalid arguments

Typically means the flags field or byte count is malformed in the set command. Memcached does not persist data, so a restart loses everything—always pair memcached with a durable store if the data is critical.

Silent failure mode: A cache key expires at exactly the moment a write occurs. The write invalidates the key, but a subsequent read sees the miss, fetches from the database, and repopulates the cache with the new value. If the invalidation message was lost (network partition, broker failure), the cache retains the old value while the database has the new one. This is a coherency split-brain scenario that is difficult to detect without explicit version checking.

Cache Warming

Cache warming pre-populates the cache with known hot keys before traffic spikes. This is common before deployments, autoscaling events, or scheduled batch loads.

Real procedure (Kubernetes + Redis):

apiVersion: batch/v1
kind: Job
metadata:
  name: cache-warmup
spec:
  template:
    spec:
      containers:
      - name: warmup
        image: myapp:latest
        command: ["sh", "-c"]
        args:
        - |
          for key in $(cat /run/secrets/warmup-keys); do
            redis-cli SET "$key" "$(pg_dump -t users -Fc | gzip | base64)" EX 86400
          done
      restartPolicy: OnFailure

The secrets file /run/secrets/warmup-keys contains newline-separated key names. The job runs once, populates the cache, and exits. If the job fails partway, some keys remain cold—acceptable if the application falls back to the database on miss.

Real SQL-based warmup (PostgreSQL + pg_catcache):

SELECT set_config('myapp.cache_warmup', 'users,orders,products', false);
SELECT pg_catalog.set_config('search_path', '', false);
CALL populate_cache();

The populate_cache function would be a stored procedure that fetches recent or frequently accessed rows and inserts them into a dedicated cache table. This pattern is less common than external caches but useful when the database engine’s own buffer pool is the target.

Monitoring and Failure Modes

Cache Miss Rate

A rising miss rate often signals TTL too short, key space too large for memory, or a traffic pattern change. Monitor get_misses vs get_hits per key prefix.

Eviction Policy

Redis defaults to allkeys-lru (least recently used across all keys). Memcached uses an LRU approximation within its configured memory cap. If your workload has a few hot keys and many cold keys, allkeys-lru will evict the hot keys under memory pressure, causing a sudden spike in database load. Use volatile-ttl instead if you can tag hot keys with a short TTL.

Stampede / Thundering Herd

When a popular key expires simultaneously, every concurrent reader misses the cache and hits the database. Mitigation:

Cache Poisoning

If an attacker can inject a key into the cache (e.g., via a vulnerable deserialization endpoint), they can return arbitrary data to legitimate users. Redis without authentication on production ports is a classic vector. Always bind Redis to 127.0.0.1 or a private network, require requirepass, and use rename-command CONFIG "" to hide the CONFIG command from unprivileged clients.

TTL Drift

In distributed cache topologies (Redis Cluster, Codis, Twemproxy), TTL calculations can drift across nodes due to clock skew or hash slot assignments. A key set with EX 300 on one node might effectively live 290 or 310 seconds on another. Use NTP across all cache nodes and verify TTL consistency with TTL key after writes.

Sharp Edges — What You Learn After It Breaks

Key Configuration Patterns (Generic)

PatternTypical KeyTypical TTL RangeWhen to Use
Hot user profileuser:{id}300–900sRead-heavy, write-rare, low coherency requirement
Session storesession:{id}1800s (30min)Must expire on logout; short-lived
Feature flagff:{flag_name}86400s (1day)Deploy-time changes; invalidate on cap rollout
Aggregated metricmetric:{hour}3600s (1hr)Time-windowed; recompute after window closes
Product catalogprod:{sku}600–1800sInfrequent updates; TTL longer than deploy cycle

No single key format is universal. The pattern is: a namespace prefix (user:, session:, ff:, metric:, prod:) followed by a scope identifier ({id}, {flag_name}, {sku}). The TTL should be shorter than the maximum acceptable staleness for that data class, plus a safety margin for invalidation delays.

Summary

Database caching layers reduce read pressure and latency, but they introduce coherency challenges that are easy to underestimate. Cache-aside is the safest starting point; write-through adds complexity with marginal benefit for most workloads; write-behind trades durability for latency. Invalidation strategy matters more than cache engine choice. TTL jitter, lock-based repopulation, and regular cache warmup jobs are the practical levers that keep a cache useful rather than harmful. Monitor miss rates, eviction policies, and connection counts between the cache and database. When the cache is correctly sized, correctly invalidated, and correctly warmed, it is an invisible acceleration. When it breaks, it becomes the bottleneck that takes the database down with it.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.