Backend / System Design Interviews

Backend Under Load: Eight Failure Modes

"Your p99 just went from 50ms to 5000ms during a traffic spike — walk me through it." Here are the eight usual suspects behind that sentence: what each one is, why it happens, how you'd catch it in metrics, how to fix it, and how they chain into each other in a real incident.

On this page

Interviewers at this level rarely want a fresh whiteboard architecture. They want to know what you do when the architecture you already drew starts to buckle — because in production, every one of these eight failure modes shows up sooner or later, usually more than one at a time. The skill being tested is symptom → correlated metric → root cause → fix, not trivia recall.

 How to read this guide
Each failure mode below follows the same shape: what it is, why it happens, how you'd catch it in metrics or logs, how to fix or prevent it, and a scenario you can talk through out loud. Red marks an active failure signal, amber a degraded/borderline one, green a healthy or fixed state — the same color language carries through every diagram.

01Connection pool saturation

Your application doesn't talk to the database directly on every request. It borrows a connection from a fixed-size pool (HikariCP in Java is the canonical example), uses it, and returns it. When every connection is checked out and requests keep arriving, new requests queue behind the pool. If they wait long enough, they time out.

Why it happens

  • Pool sized for average load, not actual peak concurrency.
  • Connections held too long — long transactions, leaked connections that never return, N+1 queries inside a single transaction.
  • A downstream slowdown (slow queries, a stalled dependency) means each connection is held longer than usual — the pool's effective throughput drops even though its size hasn't changed.
  • Fleet math is the real trap: 50 pods × a pool size of 20 offers 1,000 simultaneous connections to a database that may handle a few hundred well. The pool protects a single instance — nothing protects the database as a whole unless you design for it.
Fig. 1

Connection pool exhaustion under a traffic spike

Connection pool exhaustion under a traffic spikeIncoming requests from a traffic spike hit a connection pool of size 20 with all 20 connections checked out. New requests join a growing queue and time out after connectionTimeout, while the database itself is not the bottleneck.App instance20 in usewaitsIncomingrequeststraffic spikePool (size = 20)20 / 20 checked outqueue growingwaiting for a connectionDBnot the bottleneckTimeoutafter connectionTimeout
The database is often fine — it's the pool in front of it that runs out of seats.

Diagnosis

  • Pool metrics: active connections ≈ max pool size, plus a growing "waiting for connection" queue.
  • Symptom: SQLTransientConnectionException: Connection is not available, request timed out (HikariCP), or the equivalent driver timeout elsewhere.
  • Correlate pool exhaustion with query latency, not just request volume — often the pool wasn't undersized, the queries got slow.

Fix / prevention

  • Right-size the pool using HikariCP's starting-point formula, then tune with real load tests: connections ≈ (core_count × 2) + effective_spindle_count. Bigger isn't always better — excess connections add context-switching and contention on the DB itself.
  • Fail fast: set an aggressive connectionTimeout (2–3s) so requests error out visibly instead of queuing silently.
  • Cap total connections across the fleet, not per instance — think in terms of a database connection budget split across replicas, batch jobs, and admin tooling.
  • Put a multiplexer/proxy (PgBouncer, ProxySQL, Oracle CMAN) between app and DB to decouple "connections the app thinks it opened" from "connections the DB actually maintains." PgBouncer's transaction pooling mode in particular can serve hundreds of app connections from a much smaller real pool.
  • Turn on leak detection (HikariCP's leak-detection-threshold) to catch code paths that check out a connection and never return it.
  • Watch for autoscaling "connection storms": 50 new pods opening a full pool at boot can spike DB CPU in seconds. Stagger warm-up — start with minimum-idle: 1 and grow lazily.
HikariCP — safer defaults for a fleet
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=1
spring.datasource.hikari.connection-timeout=2500
spring.datasource.hikari.leak-detection-threshold=30000
spring.datasource.hikari.validation-timeout=1000
Scenario to reason through
A Spring Boot service behind an HPA. Traffic doubles during a flash sale; Kubernetes scales pods 20 → 80. The database itself falls over — not the app. This is a connection storm: 80 pods × pool size 10 = 800 sudden new connections. The fix isn't "raise the pool size" — it's capping total DB connections fleet-wide and warming pools gradually.

02Cache miss storm

A cache exists to shield the database from repeated reads of the same data. A cache miss storm happens when a large chunk of the cache becomes invalid or unavailable at once — a cache node restarts, a deploy flushes it, or many keys share a TTL and expire together — and a flood of requests that would normally hit the cache all fall through to the database simultaneously.

Why it happens

  • Synchronized TTLs: many keys cached at the same moment with the same expiry all die together.
  • Cold cache after deploy or restart: a new cache instance, or a cluster failover, starts empty.
  • Eviction under memory pressure: LRU eviction removes hot keys exactly when you need them most.
Fig. 2

Mass cache expiry floods the database

Mass cache expiry floods the databaseA cache cluster restarts or many keys hit their TTL together, so many different keys miss at once. Every miss lands on the database, which responds with timeouts and high latency.Many keysCache clusterrestarts / mass TTL expiryMISSMISSMISSMISSDatabaseevery miss lands hereTimeoutshigh latencyread QPS spikes
Broad and multi-key — the whole cache goes cold at once, not just one popular entry.

Diagnosis

  • Sudden, correlated spike in miss rate across many keys (contrast with Thundering Herd below, which is one key).
  • DB read QPS spikes in lockstep with a cache hit-rate drop.
  • Usually visible right after a deploy, a cache cluster resize, or a node failover.

Fix / prevention

  • Jittered TTLs — add randomness to expiry: TTL = base ± random(0, jitter), so keys don't all die at the same instant.
  • Cache warming before cutting traffic over to a new node or cluster, especially post-deploy.
  • Graceful degradation: serve slightly stale data (stale-while-revalidate) instead of forcing every reader to the DB.
  • Multi-tier caching: local in-process cache + shared Redis/Memcached, so a shared-cache blip doesn't send 100% of traffic straight to the DB.
  • Read-through with request coalescing (see Thundering Herd) so even a full flush produces one DB query per key, not one per concurrent request.

03Thundering herd

An especially vicious, narrower case of a cache miss: one single hot key expires, and every one of its many concurrent readers independently notices the miss and hits the database at the same instant to regenerate it. The name borrows from the general OS-level phenomenon where many threads blocked on the same event all wake up simultaneously, but only one can actually proceed — the rest collide and go back to sleep, wasting the cycles it took to wake them.

A widely-used mental model: a product recommendation cache key expires at scale, and tens of thousands of concurrent requests hit the primary database within seconds — CPU spikes toward saturation, checkout pages slow to a crawl, and the root cause is "just" one expired cache key.

Fig. 3

Thundering herd, with and without request coalescing

Thundering herd, with and without request coalescingWithout coalescing, a hot key expires and request 1 plus requests 2 to N all query the database, which sees N identical queries at once. With single-flighting, request 1 acquires a lock and runs one query, requests 2 to N see the lock and wait, and the cache is refreshed once for everyone to read.missmissqueryquery × Nmissmissthen readshot key expiresreq 1req 2..NDatabaseN identical queries at oncekey expiresreq 1acquires lockreq 2..Nsee lock, wait1 query onlycache refreshed once — everyone reads itWithout coalescingWith single-flighting
Same event, two outcomes: N duplicate queries, or exactly one — the only difference is whether a lock elects a single rebuilder.

Diagnosis

  • One specific key or query pattern dominates the miss spike (versus the broad, multi-key pattern of a cache miss storm).
  • DB slow query log shows the exact same query running dozens or hundreds of times concurrently.
  • Correlates precisely with the TTL of one hot object.

Fix / prevention

  • Request coalescing / single-flighting: on a miss, the first request acquires a lock and regenerates the cache; every other concurrent request for the same key waits briefly, then reads the now-fresh entry instead of also hitting the DB. The design question becomes "how do I elect a single leader to rebuild this key," not "what happens on a miss."
  • Probabilistic early expiration: refresh the key before it actually expires, with rising probability as expiry approaches — regeneration happens off the critical path, with no hard cliff.
  • Never let TTL be the only expiry mechanism for hot keys — actively refresh them in the background via cron/worker so they never truly miss.
  • Locking with a short grace period: hold a mutex per key during regeneration; other readers serve stale data for a few hundred ms rather than blocking on the DB.
Single-flighting, in pseudocode
value = cache.get(key)
if value is not None:
    return value

if acquire_lock(key, ttl=500ms):        # only one caller wins this
    try:
        value = db.query(...)
        cache.set(key, value, ttl=jittered(base_ttl))
        return value
    finally:
        release_lock(key)
else:
    # someone else is already rebuilding — wait briefly, then re-read
    sleep(50ms)
    return cache.get(key) or stale_cache.get(key)

04Lock contention

Multiple threads or processes need exclusive access to the same resource — a row, a data structure, a critical section — and end up serializing behind a lock. As concurrency rises, more time is spent waiting for the lock than doing useful work.

Why it happens

  • Coarse-grained locks — locking an entire table or object when only a small part needs protection.
  • Long-held locks — a transaction holding a row lock while doing slow, unrelated work like an external API call.
  • High contention on a small number of hot rows — everyone incrementing the same counter, many transactions updating the same inventory record.
  • In application code: synchronized blocks or mutexes guarding shared in-memory state.
Fig. 4

Threads queued behind one lock

Threads queued behind one lockThread 0 holds the lock on Row X and runs; threads 1, 2 and 3 are blocked waiting for it. Throughput is about one thread’s worth of work, regardless of how many threads exist.Threadsholds lockBLOCKEDBLOCKEDBLOCKEDserializedThread 0runningThread 1waitsThread 2waitsThread 3waitsLock onRow Xthroughput ≈ 1 thread’sworth of work, regardlessof how many threads exist
Classic tell: CPU can look low even though latency is high — the bottleneck is waiting, not computing.

Diagnosis

  • Thread dumps show many threads BLOCKED waiting on the same monitor/lock.
  • Database: SHOW ENGINE INNODB STATUS (MySQL) or pg_locks (Postgres) reveals long lock-wait chains and deadlock victims.
  • CPU utilization can look low even though latency is high — a classic tell that the bottleneck is waiting, not computing.
  • Throughput plateaus or drops even as you add more threads or instances — Amdahl's Law in action, the serialized portion dominates.

Fix / prevention

  • Reduce lock scope and hold time: do slow work (I/O, external calls) outside the critical section.
  • Move from pessimistic to optimistic concurrency control where contention is high but conflicts are actually rare — versioned rows / compare-and-swap instead of SELECT ... FOR UPDATE.
  • Shard the contended resource: instead of one counter row, use N counter shards and sum them (same principle as hot partitions, below).
  • Use appropriate isolation levels: don't default to the strictest level everywhere — know your actual consistency requirement.
  • Queue-based serialization: if a resource genuinely must update one-at-a-time, make that explicit via a queue rather than many threads fighting over a lock.

05Hot partitions

In a sharded or partitioned data store (DynamoDB, Cassandra, sharded Postgres/MySQL), data spreads across nodes by hashing a partition key. A hot partition happens when a disproportionate share of traffic lands on one partition — because one key, or a small set of keys, is far more popular than the rest — even though the cluster as a whole has spare capacity.

Why it happens

  • Low-cardinality keys: partitioning by something like status = "ACTIVE" or a date-only value concentrates huge volume on very few partition values.
  • Celebrity effect: one user, one product, one leaderboard entry gets orders of magnitude more reads/writes than everyone else.
  • Time-series data: keying by deviceId or a fixed date bucket means all of "today's" writes land on the same physical partition, since time-ordered data inherently targets one logical key repeatedly.
  • Per-partition limits are hard limits — in DynamoDB, a single partition tops out around 3,000 RCU / 1,000 WCU regardless of how much total capacity the table has provisioned, so a hot partition throttles even when the table "looks" underutilized.
Fig. 5

One partition absorbing most of the traffic

One partition absorbing most of the trafficA table has 10,000 WCU in total and 1,000 WCU per partition. Partitions 1, 2 and 4 use about 200, 180 and 150 WCU, while 90% of client traffic targets one key on Partition 3, whose demand of 5,000 WCU exceeds its 1,000 WCU limit.Table90%Clients90% of client traffic targets one keyPartition 1limit: 1,000 WCU~200 WCUPartition 2limit: 1,000 WCU~180 WCUPartition 3 (HOT)limit: 1,000 WCUdemand: 5,000 WCUPartition 4limit: 1,000 WCU~150 WCUthrottledTable: 10,000 WCU total, 1,000 WCU per partition
The table looks underutilized in aggregate — the throttling only shows up per-partition.

Diagnosis

  • Throttling errors (e.g. ProvisionedThroughputExceededException) even though aggregate table/cluster metrics show spare capacity.
  • Per-key traffic visibility tooling (CloudWatch Contributor Insights for DynamoDB, or equivalent) to identify which keys are hot before fixing anything blindly.

Fix / prevention

  • Write sharding: append a random or hashed suffix to the hot key (user#123 → user#123#shard3) so writes spread across N physical partitions; fan out reads across all shards and aggregate.
  • Composite keys with higher cardinality: combine a low-cardinality attribute with a high-cardinality one — "ACTIVE#customer123" instead of just "ACTIVE".
  • Shard selectively: you don't need to shard the whole keyspace — only the specific hot entities (e.g. the top 100 leaderboard users), leaving the long tail on a simple single-key design.
  • Cache in front of the hot partition (DAX/Redis) so the partition itself only sees a fraction of true read volume.
  • Time-bucketing for time-series: roll into new partition keys per time window (per hour/day) instead of one key growing forever.
  • Built-in mechanisms like DynamoDB Adaptive Capacity help automatically, but take several minutes to kick in — not fast enough to save you from a sudden spike. Proactive key design still matters.
Write-sharded key, read-side fan-in
// write: spread across N shards
shard = random(0, N-1)
put_item(pk=f"user#{userId}#shard{shard}", ...)

// read: fan out and merge
results = [get_item(pk=f"user#{userId}#shard{i}") for i in range(N)]
merged = merge(results)

06Slow queries

The most direct root cause on this list — a query that takes far longer than it should, tying up connections, locks, and CPU/IO on the database, and cascading into every other failure mode above.

Why it happens

  • Missing or unused indexes, causing full table scans.
  • N+1 query patterns from ORMs — one query for a list, then one query per item for related data.
  • Poor query plans from stale statistics.
  • Large, unbounded result sets — SELECT * with no pagination or limit.
  • Lock waits disguised as slow queries — the query itself is fast, but it's queued behind another transaction's lock.
  • Open-session-in-view (common in Spring) keeping a DB transaction open for the entire HTTP request lifecycle, including slow downstream calls — so otherwise-fast queries hold connections far longer than necessary.

Diagnosis

  • Slow query logs — slow_query_log in MySQL, log_min_duration_statement in Postgres.
  • EXPLAIN ANALYZE to check for sequential scans where an index scan was expected.
  • APM traces showing time spent in DB calls vs. application logic.
  • Correlate a rise in average query duration with the rest of the cascade — very often the actual root cause behind "pool exhausted" or "lock wait timeouts spiked."

Fix / prevention

  • Add/verify indexes matching actual query predicates and sort orders.
  • Fix N+1 patterns with eager loading or batched fetches.
  • Set query timeouts so one bad query can't monopolize a connection indefinitely.
  • Paginate everything; never return an unbounded result set.
  • Disable open-session-in-view where it's not needed; keep transactions as short as possible.
  • Use read replicas to offload read-heavy queries away from the primary.

07Network bottlenecks

Latency or throughput limits introduced by the network path between services, not the services themselves: bandwidth saturation, DNS resolution delays, TLS handshake overhead, cross-region/cross-AZ hops, load balancer or proxy limits.

Why it happens

  • Chatty microservices making many small synchronous calls instead of batching.
  • Cross-region calls adding tens to hundreds of ms of unavoidable latency per hop.
  • Load balancer health checks or connection limits misconfigured — healthy instances marked unhealthy, or connections dropped.
  • NIC/bandwidth saturation on a single host under heavy fan-out — broadcasting to many downstream services at once.
  • DNS caching issues causing repeated, slow lookups under load.
Fig. 6

Every hop adds latency; one bad hop can eject healthy instances

Every hop adds latency; one bad hop can eject healthy instancesA request travels Client, DNS, load balancer, Service A, then cross-AZ to Service B and cross-region to Service C in another region. A single slow or misconfigured hop, such as a bad health check, can eject otherwise-healthy instances downstream.bad health checkClientLBService AService BService Cother regionOne bad hopA single slow/misconfigured hop (e.g. a bad health check) can eject otherwise-healthy instances downstream.every hop adds latencyDNSroutecross-AZcross-region
Distributed tracing turns this row of boxes into a waterfall — the wide gaps are where the network, not the code, is spending time.

Diagnosis

  • Compare "time in app code" vs. "time in transit" using distributed tracing — a trace waterfall showing large gaps between spans.
  • Load balancer metrics: active connections, backend error rate, health check failures.
  • ping / traceroute / mtr for raw latency and packet loss where you can debug directly.
  • Rule out DNS — slow or failing resolution often masquerades as a generic "connection timeout."

Fix / prevention

  • Reduce cross-service chattiness: batch requests, use gRPC/streaming instead of many round trips.
  • Co-locate latency-sensitive services in the same AZ/region.
  • Tune load balancer health check intervals/thresholds so transient blips don't cause mass instance ejection.
  • Use connection keep-alive and HTTP/2 multiplexing to avoid repeated handshake overhead.
  • Circuit breakers and timeouts on every outbound call, so one slow network path doesn't stall the whole request chain.

08GC pauses

In managed-memory runtimes (JVM, .NET CLR, Go's GC), the garbage collector periodically pauses application threads ("stop-the-world") to reclaim memory. Under memory pressure or with a poorly tuned collector, these pauses grow long and frequent enough to show up as latency spikes — and in extreme cases, cause the process to be marked unhealthy by orchestration or a load balancer.

Why it happens

  • Heap sized too small for the actual working set, forcing frequent collections.
  • High allocation rate — lots of short-lived object churn overwhelming the young generation.
  • Memory leaks — objects that should be garbage but are still referenced, growing the old generation until a full GC is needed.
  • An old/default collector (Parallel GC) used for a latency-sensitive service instead of a low-pause one (G1, ZGC, Shenandoah).
  • Large heaps paradoxically making full-GC pauses worse if not tuned for concurrent/incremental collection.
Fig. 7

Request latency around a stop-the-world GC pause

Request latency around a stop-the-world GC pauseRequests A and B complete quickly before the pause. A stop-the-world GC pause then halts application threads, so Requests C and D are blocked and their latency spikes once the pause ends.p99 latencyRequest AfastRequest BfastRequest Cblocked, then spikesRequest Dblocked, then spikesblockedblockedtime →app threads pausedStop-the-world GC pausespike
The smoking gun in most postmortems: p99/p999 latency spikes line up exactly with GC pause timestamps.

Diagnosis

  • GC logs (-Xlog:gc* on modern JVMs) showing pause duration and frequency.
  • Correlate p99/p999 latency spikes with GC pause timestamps.
  • Heap dumps / profilers (async-profiler, VisualVM) to find allocation hotspots or leaks.
  • Kubernetes liveness-probe failures that line up exactly with long GC pauses — the process is "alive" but unresponsive during STW.

Fix / prevention

  • Choose a low-pause collector for latency-sensitive services — G1 as a solid default, ZGC/Shenandoah for very large heaps needing sub-millisecond pauses.
  • Right-size the heap: too small causes GC thrashing, too large without a concurrent collector causes long full-GC pauses.
  • Reduce allocation pressure — object pooling on very hot paths, avoiding unnecessary boxing, streaming instead of buffering large payloads.
  • Fix leaks — common culprits are unbounded caches, listeners/callbacks never unregistered, ThreadLocals never cleared.
  • Set liveness-probe thresholds with GC behavior in mind, so a normal (if long) pause doesn't trigger an unnecessary pod restart that compounds the problem.
A reasonable G1 starting point
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+ParallelRefProcEnabled
-Xlog:gc*:file=/var/log/app/gc.log:time,level,tags

09The interview diagnostic checklist

When an interviewer says "the service is slow, walk me through debugging it," this is the mental checklist worth narrating out loud, roughly in order:

  1. CPU check karoIs CPU actually maxed out, or low despite high latency? Low CPU + high latency usually means waiting — locks, I/O, network — not computing.
  2. Memory check karoIs the process near its memory limit? Climbing steadily (a leak) or spiking in bursts (an allocation storm)?
  3. Garbage collection pauses dekhoCorrelate GC pause timestamps with latency spikes — often the fastest way to confirm or rule out GC as the culprit.
  4. Network latency verify karoUse distributed tracing to see how much time is in the network vs. application code. Check cross-AZ/region hops specifically.
  5. Load balancer healthy hai ya nahiCheck LB-level metrics — are backend instances flapping between healthy and unhealthy? Are health check failures correlated with GC pauses or pool exhaustion, i.e. is the LB reacting to a downstream problem rather than causing one?
What's actually being evaluated
Not whether you've memorized this list — whether you reason from symptom → correlated metrics → root cause → fix, and understand how these eight failure modes chain into and mask each other.

10How these failures cascade

In a real incident, these almost never happen in isolation. A typical chain:

Fig. 8

A traffic spike cascading through all eight failure modes and back to itself

A traffic spike cascading through all eight failure modes and back to itselfA traffic spike causes a cache miss storm or thundering herd, slow queries flood the database, lock contention queues transactions, the connection pool saturates fleet-wide, GC worsens and the load balancer ejects slow instances, and the remaining instances take more load, feeding the spike.remaining instancestake more loadTraffic spike1Cache miss storm /thundering herd2Slow queriesflood the DB3Lock contentionqueues transactions4Connection poolsaturates fleet-wide5GC worsens, LB ejectsslow instances6the loop closes on itself
The loop closes on itself: ejecting slow instances concentrates load on the survivors, deepening the same spike that started it.

This is why a strong interview answer names the failure mode you'd check first, but also explains how you'd separate root cause from downstream symptom — for example: "pool exhaustion is often not a pool-sizing problem, it's a symptom of slow queries or lock contention upstream."

11References

This guide draws on official documentation (AWS, Oracle/OpenJDK, PostgreSQL, MySQL) and independent engineering write-ups. For interview practice specifically, cross-reference against more than one prep source and real interview-experience threads — question patterns and interviewer expectations vary by company and change over time.

Connection pool saturation
  • HikariCP connection pool tuning guidance for Spring Boot
  • Database connection pool tuning under traffic spikes (HikariCP + PostgreSQL)
  • Production-oriented Hikari connection pool guides
Cache miss storm / thundering herd
  • Thundering herd problem — general OS/networking background
  • Cache stampede — background and terminology
  • Cache stampede mitigation write-ups (Redis-focused)
Lock contention
  • MySQL — SHOW ENGINE INNODB STATUS documentation
  • PostgreSQL — pg_locks system view documentation
Hot partitions
  • AWS DynamoDB Developer Guide — key-range throughput exceeded / hot partition mitigation
  • DynamoDB hot partition explainers and partition/sharding write-ups
Slow queries
  • PostgreSQL EXPLAIN documentation
  • MySQL slow query log documentation
Network bottlenecks
  • AWS Well-Architected Framework — Performance Efficiency pillar
GC pauses
  • Oracle JVM garbage collection tuning guide
  • G1 garbage collector overview (Oracle)
  • ZGC — OpenJDK wiki