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.
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 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 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.”
Invalidation is where most production incidents originate. Three primary patterns exist:
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 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.
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.
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.
When a popular key expires simultaneously, every concurrent reader misses the cache and hits the database. Mitigation:
base_ttl + random(0, jitter). No two keys expire at exactly the same second.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.
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.
POST /purge). Application-level caches that sit in-process or beside the app have a more accurate view of state but add latency per request.| Pattern | Typical Key | Typical TTL Range | When to Use |
|---|---|---|---|
| Hot user profile | user:{id} | 300–900s | Read-heavy, write-rare, low coherency requirement |
| Session store | session:{id} | 1800s (30min) | Must expire on logout; short-lived |
| Feature flag | ff:{flag_name} | 86400s (1day) | Deploy-time changes; invalidate on cap rollout |
| Aggregated metric | metric:{hour} | 3600s (1hr) | Time-windowed; recompute after window closes |
| Product catalog | prod:{sku} | 600–1800s | Infrequent 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.
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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy