Scalability Patterns and Techniques Questions
Scaling a system to handle growth in traffic and data: horizontal versus vertical scaling, statelessness, sharding and partitioning strategies, read replicas, and connection pooling. Covers capacity estimation, identifying bottlenecks, and the tradeoffs each scaling axis introduces. The general toolkit for taking a design from thousands to millions of users.
A user request traverses six microservices. How would you measure and attribute its P95/P99 tail latency, and what would you do to reduce it? Cover your instrumentation and sampling/tracing strategy, how you'd detect a spike, and mitigation techniques such as hedged requests, request prioritization, resource partitioning, and admission control.
Sample Answer
Direct answer
Measuring tail latency across six hops means separating two questions: which hop is actually responsible for a given slow request (attribution), and is the tail getting worse over time (detection). Attribution needs per-hop distributed tracing with sampling that preserves slow traces even when it drops fast ones; detection needs P95/P99 (95th- and 99th-percentile latency, the response times only the slowest 5% and 1% of requests exceed) tracked as their own alertable series, since a stable median (this is the common trap: P99 spikes while the median looks completely healthy) hides exactly this class of problem. Once a hop is identified, the fix is rarely "make everything faster" but a targeted mitigation such as hedged requests, request prioritization, resource partitioning, or admission control aimed at that specific hop, each of which trades some cost or complexity for the latency it buys back.
Measurement and attribution
- Instrumentation: every one of the six services emits a span per request with start/end timestamps, propagated trace context, and enough metadata (host, downstream call outcome, queue wait time) to distinguish "this hop was slow" from "this hop was waiting on the next one."
- Sampling strategy: pure random sampling at low rates (say 1%) will almost never happen to capture a P99 request, since by definition only 1% of requests qualify and the sample and the tail rarely overlap. Use tail-preserving (tail-based) sampling: buffer a trace briefly and only decide to keep it once you know whether any span exceeded a latency threshold, so slow traces are captured close to 100% of the time while typical traces are still sampled cheaply.
- Attribution: once a slow trace is captured, break its total duration into a waterfall of per-hop contributions to see which hop consumed the largest share.
- Spike detection: alert on the rate of change of P95/P99 against a rolling baseline (for example, a sustained jump relative to the trailing window), not on a single fixed threshold, since normal traffic variation would otherwise cause constant false alarms.
Worked example: attributing a P99 spike across six hops
As an illustrative example, not measured data, suppose a captured slow trace shows an 800 ms end-to-end duration split across the six hops as follows:
| Hop | Contribution to trace duration |
|---|---|
| A (edge/gateway) | 50 ms |
| B (auth/lookup service) | 300 ms |
| C (business logic) | 100 ms |
| D (data-access service) | 150 ms |
| E (enrichment service) | 100 ms |
| F (response assembly) | 100 ms |
| Total | 50+300+100+150+100+100 = 800 ms |
Hop B accounts for 800300=0.375=37.5% of the total, the single largest share, so it is the first place to investigate and the first place a mitigation should target, rather than spreading effort evenly across all six services.
Mitigation techniques, their overhead, and their risk
| Technique | What it does | Operational overhead | Risk |
|---|---|---|---|
| Hedged requests (replica hedging) | Send a second request to a different replica after a short delay if the first hasn't responded; take whichever finishes first and cancel the other | Requires idempotent operations and extra downstream capacity headroom to absorb the duplicate load | Can amplify load during a genuine overload, since a slow dependency triggers hedges everywhere at once; needs a cap on hedge rate or it makes the underlying problem worse |
| Request prioritization (priority queues) | Classify requests as interactive versus batch and schedule interactive traffic ahead of batch at every hop | Every hop in the path must honor the same priority scheme consistently, adding coordination and scheduling complexity | Low-priority traffic can starve entirely if there's no guaranteed minimum share for it |
| Resource partitioning (resource isolation) | Dedicate CPU/memory pools to latency-sensitive services so a noisy batch workload can't steal their resources | Deliberately reduces overall utilization efficiency in exchange for isolation, and adds infrastructure to manage separately | If the partitions are too small or the isolation boundary is drawn at the wrong level, the workloads you meant to separate can still interfere |
| CPU pinning | Bind a hot service's threads to specific cores to reduce cross-core cache misses and scheduler-induced jitter | Removes the scheduler's flexibility to pack other work onto those cores, reducing overall efficiency | Pinning to cores that still share a memory controller or cache with a noisy neighbor gives no benefit while still costing the flexibility; needs revisiting if hardware topology changes |
| GC tuning (garbage-collection tuning) | Reduce allocation rate and favor a pause-time-oriented garbage collector so tail latency isn't dominated by stop-the-world pauses | Requires runtime-specific expertise and ongoing revalidation as code and allocation patterns evolve | Trading pause time for throughput is a real trade, not a free win; a poorly chosen configuration can make both worse |
| Avoiding blocking I/O | Perform network and disk calls asynchronously so a thread isn't held idle waiting on a slow dependency | Async code is harder to write, test, and debug: error propagation and cancellation get more complex | Can hide backpressure (a signal that would otherwise tell the caller to slow down because you can't keep up) if not paired with bounded queues, since a service can accept far more concurrent work than it can actually finish in time |
| Admission control | Reject or shed excess load at the edge before it enters the six-hop path | Needs per-tenant or per-class quotas and clear client-facing signaling (retry-after style responses) | Overly aggressive shedding converts a latency problem into an availability problem for legitimate traffic |
Trade-offs and pitfalls
- Chasing every hop at once instead of attributing first. Without the waterfall breakdown, teams tend to optimize the hop that's easiest to touch rather than the one actually driving the P99.
- Sampling uniformly at a low rate and concluding tail latency "looks fine" because slow traces were simply never captured. The sampling strategy has to be tail-aware, not just cheap.
- Applying hedging without a cap. It is the mitigation most likely to backfire under genuine overload, since it adds load exactly when the system can least afford it.
- Treating any single mitigation here as free. Every row in the table above buys latency at the cost of either infrastructure efficiency, code complexity, or operational risk; picking one should follow from what the attribution step actually showed, not from familiarity with the technique.
Compare range-based, hash-based, and directory-based sharding strategies for partitioning write-heavy user data. Discuss the pros and cons of each in terms of hotspot formation, rebalancing complexity when adding nodes, support for range queries, and impact on secondary indexes and transactions.
Sample Answer
Direct answer
Range-based sharding groups keys into contiguous ranges (good for range queries, prone to hotspots on the newest range); hash-based sharding spreads keys evenly by hashing them (avoids that hotspot but destroys ordering, so range queries scatter across every shard); directory-based sharding uses a lookup service to map each key to a shard explicitly (most flexible for steering around hotspots, at the cost of operating and scaling that lookup service). For write-heavy user data, the right starting point is almost always hash-based, layered with directory-style overrides for the specific keys that turn out to be hot in practice.
Structured elaboration
At the simplest level, the choice comes down to one question: does your access pattern need range queries, or does it need even load distribution? Those two goals are usually in tension, which is why the three strategies exist.
| Dimension | Range-based | Hash-based | Directory-based |
|---|---|---|---|
| Hotspot formation | High risk: a shard holding the newest or most-active range absorbs disproportionate write traffic | Low risk for aggregate load, since keys spread evenly; a single very-hot key still hits one shard regardless of hashing | Lowest risk in practice: a hot key can be explicitly moved or replicated to relieve its shard |
| Rebalancing when adding nodes | Moderate to costly: splitting a range means physically moving a large contiguous block of data | With plain modulo hashing, adding a node reshuffles most keys; with consistent hashing, only keys near the new node's position move | Cheapest to reason about: update the directory entry for the affected keys; the underlying data still has to move, but only for the keys chosen, not a bulk range or hash-bucket shift |
| Range queries | Efficient: a scan over an ordered key range touches only the shards holding that range | Poor: hashing destroys order, so a range scan has to fan out to every shard and merge results | Depends on the directory's mapping scheme: efficient if it preserves contiguous mappings, expensive if mappings are arbitrary |
| Secondary indexes | Simple if the index key correlates with the shard range; otherwise needs scatter-gather (fan the query out to every shard, then merge the partial results into one answer) | Harder: a global secondary index generally needs either per-shard indexes aggregated at query time, or a separate index service | Can centralize index ownership through the same directory, but that adds another responsibility to the lookup service |
| Transactions | Good for range-local transactions; anything spanning ranges needs a distributed transaction | Single-key transactions stay on one shard; multi-key transactions usually span shards unless keys are deliberately co-located | Best control: related keys can be deliberately mapped to the same shard to keep transactions local, but only as reliably as the directory is kept consistent |
Alternate shard-key dimensions. User ID is not the only reasonable key for user data. Two common alternatives, each changing the hotspot and locality picture:
- Partitioning by tenant (for a multi-tenant product): every row for one customer organization lives together, which keeps that customer's queries and transactions shard-local, but a single very large tenant can become a hot shard on its own regardless of hashing, and tenant sizes are rarely uniform.
- Partitioning by geographic region: keeps a region's data physically close to the users generating it and can satisfy data-residency requirements, but regions do not generate even load (one region's business hours are another's overnight lull), so this dimension needs the same hotspot vigilance as range-based sharding by time.
Either alternative needs a request-routing design that resolves a given request to the correct partition before any query executes: for tenant or region keys this is usually a straightforward lookup (tenant ID and region are typically known before the query is issued), but it is still an explicit routing decision, not something a plain hash of an opaque ID gives you for free.
A simpler way to hold the whole comparison in your head: shard-key choice is really a load-distribution and data-locality decision. If you need data that is accessed together to live together (range scans, tenant-local transactions, region-local latency), you are choosing locality over even distribution, and range- or directory-based sharding fits. If you need writes spread as evenly as possible and do not need that locality, hash-based sharding fits.
Worked example
A write-heavy user-activity table (one row per user action, high insert rate) is first sharded by signup-date range, since that was the easiest key to reach for. Under load, the shard holding "this week's" range becomes visibly hotter than the others, since all new activity lands there, exactly the hotspot risk in the table above. Switching the shard key to hash(user_id) spreads new writes evenly across all shards, because a hash has no notion of "this week." The cost: a query like "all activity in the last 7 days across all users" now has to fan out to every shard and merge results, whereas it used to touch one or two. If that query is rare and the write hotspot was the actual production problem, the hash-based trade is the right one; if that range query is core to the product, a directory-based approach (route the most recent, hottest range's writes across several shards explicitly, while keeping most historical data range-organized) is worth the added operational complexity instead.
Trade-offs & pitfalls
- Consistent hashing (not plain modulo hashing) is what makes hash-based sharding practical to rebalance; naive
hash(key) % Nreshuffles nearly everything whenNchanges, which erases the rebalancing advantage hashing is chosen for. - Directory-based sharding trades data-movement cost for a new single point of scaling and consistency risk: the directory service itself must be highly available and kept correct, or every lookup through it is wrong.
- Secondary indexes are the detail teams most often discover too late: a design that looks fine for primary-key access can require an expensive scatter-gather or a whole extra index service the moment a "find by email" or "find by status" query shows up.
- Choosing tenant or region as the shard key without checking size distribution first is a common wrong turn: one outsized tenant or one high-traffic region can silently become the new single hot shard, defeating the purpose of sharding at all.
At scale, a centralized lock can become a bottleneck for high-throughput, write-heavy workloads. What architectural alternatives would you reach for instead? Discuss optimistic concurrency, partitioned ownership, sequence-based approaches, and application-level conflict resolution, and explain how each preserves correctness without a central lock.
Sample Answer
Direct answer
A centralized lock caps throughput at whatever one coordinator can serialize, so the fix is to stop needing a single point of serialization: use optimistic concurrency where conflicts are rare, partition ownership so most operations never contend at all, replace "acquire before act" with a monotonically increasing sequence that orders operations after the fact, or push conflict resolution into the application where domain knowledge can merge concurrent updates without blocking either writer.
Structured elaboration
Optimistic concurrency control. Instead of acquiring a lock before reading and writing, read a version (a row version number or timestamp), do the work locally, then write conditionally: "update only if the version is still what I read." If another writer got there first, the write is rejected and the operation retries. This preserves correctness (exactly one of the racing writers wins, the loser sees a definitive failure rather than silent overwrite) without ever blocking a reader or holding a lock across the work. It is a good fit when conflicts are the exception, not the rule; under high contention the retry rate itself becomes the bottleneck.
Partitioned ownership. Instead of one lock guarding the whole resource, split the resource (by key range, by tenant, by shard) so each partition has its own independent point of coordination, or no coordination need at all if each partition is owned by exactly one writer. Correctness comes from the partitioning key guaranteeing that concurrent operations on different partitions never touch the same data; cross-partition operations are the residual hard case and usually need one of the other three approaches or a higher-level transaction pattern.
Sequence-based approaches. Instead of gating access before the fact, assign every operation a monotonically increasing sequence number (from a counter, a log offset, or a timestamp with a tie-breaker) and let operations apply in sequence order, detecting and resolving out-of-order arrivals after the fact rather than blocking to prevent them. This is the pattern behind append-only logs and event sourcing: writers never wait for each other, and correctness comes from every reader processing the sequence in the same order.
Application-level conflict resolution. When two writers legitimately modify the same data concurrently and neither should simply lose, encode a merge rule in the application: last-write-wins with a well-defined tiebreak, a domain-specific merge (like summing two concurrent counter increments instead of picking one), or a conflict-free replicated data type whose merge function is mathematically guaranteed to converge regardless of arrival order. Correctness here means "any order of application produces the same final result," which is a stronger and harder-to-verify property than the other three approaches, but it is the only one of the four that never rejects or retries a write.
Worked example
Assume, as a planning input illustrating the shape of the trade-off rather than a measured fact: a single mutex-protected counter, serialized through one lock, sustains roughly 500 operations/sec before lock contention and context-switch overhead dominate. Partitioning it into 16 independent shard counters, each capable of the same per-shard throughput since they no longer share a lock, gives:
16×500=8,000 operations/sec aggregateThe cost of that 16x gain is that no single read sees an exact real-time global total; a caller who needs the true total sums all 16 shards, which is eventually consistent with in-flight writes rather than instantaneously exact. That is the general shape of the trade-off across all four techniques: throughput is bought by relaxing some property (blocking-free correctness for retries, or a single serialization point for a scan-and-sum read) that the centralized lock previously gave you for free.
Trade-offs & pitfalls
Optimistic concurrency degrades under high contention (retries compound instead of resolving), so it is the wrong default for a hot key with many concurrent writers, exactly the case a centralized lock was protecting. Partitioning shifts, rather than removes, the hard problem to any operation that spans partitions. Sequence-based approaches move complexity from "prevent the race" to "make every consumer correctly handle re-ordering and idempotent replay," which is a real engineering cost, not a free lunch. Application-level merge functions are the hardest to get right and the easiest to get subtly wrong (a merge rule that is not truly associative and commutative will produce different results depending on arrival order, defeating the whole point); they deserve the most scrutiny and the smallest blast radius when first deployed. The senior-level judgment call is matching contention pattern to technique, not defaulting to one everywhere.
You need to decompose a monolithic application into microservices. Walk through a pragmatic approach: how you'd identify service boundaries, ensure data integrity during the migration, avoid distributed-transaction anti-patterns, and choose between orchestration and choreography. When would you reach for the strangler fig pattern?
Sample Answer
Direct answer
A pragmatic decomposition starts from real bounded contexts, not code modules, keeps data integrity intact by translating between the old and new models instead of cutting over all at once, and replaces any temptation toward a distributed two-phase commit with sagas built from idempotent, compensable steps. The strangler fig pattern, routing traffic through a facade so old and new implementations coexist during the transition, is the mechanism that makes all of this incremental and reversible rather than a risky big-bang cutover.
Identifying service boundaries
Use domain-driven design to find bounded contexts along business-capability lines (orders, billing, inventory) rather than along existing code-module lines, which usually reflect implementation history more than the current business domain. Within a candidate boundary, look for a vertical slice: API (application programming interface), business logic, and the data that logic owns, so the resulting service can operate independently without still calling back into the monolith for its own core data. Start with a boundary that is both low-risk (not on the critical write path) and clearly bounded (its data isn't heavily shared with other domains), since that combination validates the migration mechanics without betting the business on the first attempt.
Data integrity during migration
- Anti-corruption layer (ACL): a translation layer between the monolith's data model and the new service's API, so the new service isn't forced to adopt the monolith's legacy shape, and the monolith doesn't need to understand the new service's internals.
- Change data capture (CDC): stream changes from the monolith's database into the new service so reads stay consistent while write ownership is still transitioning.
- Dual writes, done carefully: if the monolith and the new service both need to write during the transition, make every write idempotent and reconcile the two stores regularly; prefer event-driven replication (the monolith publishes a change event that the new service consumes) over synchronous dual-writes, which fail if either side is briefly unavailable.
Avoiding distributed-transaction anti-patterns
Don't reach for two-phase commit (2PC) across the new service boundary. It requires every participant to be available and responsive during the commit window, which directly works against the availability and independent-deployability goals that motivated decomposition in the first place. Instead, model multi-step business flows as sagas: a sequence of local transactions, each with a defined compensating action if a later step fails, so the overall flow degrades gracefully instead of blocking on a distributed lock.
| Orchestration | Choreography | |
|---|---|---|
| Control flow | A central coordinator (a saga orchestrator) explicitly sequences each step and its compensation | Each service reacts to events from the previous step and publishes its own; no central coordinator |
| Best fit | Complex flows with non-trivial compensation logic, where you need one place to see and control the whole sequence | Simple, decoupled flows where steps genuinely don't need central sequencing |
| Observability | Easier: the orchestrator's state machine shows exactly where a given flow is | Harder: the flow is implicit in the pattern of events, so tracing a specific request end to end takes more tooling |
| Coupling | Services stay decoupled from each other but are coupled to the orchestrator's contract | Services are decoupled from any coordinator but implicitly coupled to the event schema and each other's reactions |
A common practical split is to orchestrate the core, high-stakes transaction (say, order to payment to shipment) where compensations matter, and let secondary, lower-stakes integrations (send a confirmation email, update an analytics feed) stay choreographed.
When to reach for the strangler fig pattern
Reach for it whenever you need the old and new implementations to coexist safely during a transition, which in practice is almost always for anything beyond a trivial, low-traffic domain. The mechanics, applied specifically at a monolith's gateway:
- Reverse-proxy routing: put a reverse proxy or API gateway in front of both the monolith and the new service, and route requests to one or the other by path, feature flag, or percentage, so the switch is a routing change rather than a deployment event.
- Traffic shadowing: mirror a copy of live production requests to the new service without using its response, so you can compare its behavior and performance against the monolith's real response under real traffic before the new service is trusted to actually serve anyone.
flowchart LR
C[Client] --> RP[Reverse proxy /<br/>API gateway]
RP -->|serves response| M[Monolith]
RP -.->|mirrored, response discarded| NS[New service]
RP -->|once validated,<br/>cut over by %| NS
Only retire the monolith's code path for a given slice once traffic has been fully cut over and the data behind it has been reconciled, not merely once the new service has been deployed.
Worked example: extracting a payments capability
Define the Payments bounded context and its API contract, then stand up an ACL that maps the monolith's existing payment calls onto that contract. Use CDC to populate the new Payments service's data store from the monolith's database so both stay consistent during the transition. Put a reverse proxy in front of the payments endpoints and start with traffic shadowing: every real payment request is also sent to the new service, its result compared against the monolith's, but only the monolith's response reaches the client. During this shadow period, downstream systems the payments path calls (a fraud check, a ledger write) effectively see close to double their steady-state call volume, since both the monolith and the shadowed new service are calling them, which is worth flagging to whoever owns that downstream capacity before shadowing begins. Once results match consistently, cut real traffic over gradually (a small percentage, then a majority, then all of it) via the reverse proxy, with the monolith's code path kept in place and able to take traffic back at any stage until the migration is fully reconciled.
Trade-offs and pitfalls
- Treating dual writes as sufficient data-integrity strategy on their own, without idempotency keys or a reconciliation job; a single missed or duplicated write during the transition window leaves the two stores silently inconsistent.
- Reaching for orchestration everywhere out of caution; it adds a coordinator every downstream service must trust and depend on, which is unnecessary overhead for flows that don't actually need centralized compensation logic.
- Shadowing traffic without accounting for its load on shared downstream dependencies, which can turn a validation exercise into a self-inflicted capacity incident.
- Declaring the migration done once the new service is receiving 100% of traffic, while the monolith's old code path and data are still live as a fallback; the migration isn't actually finished, and the risk isn't actually retired, until that fallback is deliberately removed.
Design a backpressure strategy for a multi-stage processing pipeline: ingest, enrich, transform, store. What sensors would you monitor (queue depths, processing latency), what thresholds would trigger backpressure, how would pressure be communicated between stages, and how would you gracefully shed load or degrade when a downstream stage is overloaded?
Sample Answer
Direct answer
A backpressure strategy for a four-stage pipeline (ingest, enrich, transform, store) needs three things working together: per-stage sensors that expose queue depth and processing latency as real signals, a control channel that lets a struggling stage tell the stage feeding it to slow down, and a graceful-shedding policy that decides what to drop or degrade when slowing down isn't enough. The same shape applies whether the pipeline moves generic records or, as a common special case, real-time features into a model-serving system: the sensors and thresholds change, but the pattern of sense, signal, shed does not.
Pipeline and signal flow
flowchart LR
ING[Ingest] --> ENR[Enrich]
ENR --> TRA[Transform]
TRA --> STO[Store]
STO -.->|backpressure signal| TRA
TRA -.->|backpressure signal| ENR
ENR -.->|backpressure signal| ING
ING -.->|reject/throttle| CLI[Clients / producers]
Sensors to monitor per stage
- Queue depth: how many items are waiting for this stage, as an absolute count and as a percentage of the stage's configured capacity.
- Processing latency: P50/P95/P99 (50th-, 95th-, and 99th-percentile response time) for one item to move through the stage.
- Throughput: items processed per second, compared against items arriving per second.
- Error and drop rate: how much work is failing or already being shed.
- Resource pressure: CPU, memory, and (for compute-heavy stages) accelerator utilization.
- Time since last successful item: catches a stalled stage that still shows an empty queue because nothing is arriving, which a queue-depth-only view would miss.
Worked example: deriving thresholds from a stated capacity
As an illustrative assumption, not a measured figure, suppose a stage's queue is configured with a capacity of 10,000 items. A common operational pattern is to define staged thresholds as percentages of that capacity:
soft-warning threshold=0.60×10,000=6,000 items
backpressure trigger=0.85×10,000=8,500 items
panic trigger=1.00×10,000=10,000 items (queue full)
Crossing 6,000 starts shedding non-essential work quietly; crossing 8,500 sends an explicit backpressure signal to the previous stage and reduces the rate it's allowed to send; hitting 10,000 forces immediate rejection of new work at this stage, since there is nowhere left to put it. The exact percentages are a policy choice, not a law, but deriving them from the stage's actual configured capacity (rather than picking round numbers with no relation to it) keeps them meaningful as that capacity changes.
Communication between stages
- Control-plane signals: each stage exposes a small control interface (or heartbeats carrying its queue depth and latency) that the stage feeding it polls or subscribes to, so PAUSE/RESUME-style signals and target-rate adjustments can propagate without going through the primary data path.
- Token-based flow control: the upstream stage requests permission (a token) before pushing an item; the downstream stage reduces the rate of tokens it issues as its own pressure rises, which caps upstream send rate without needing a synchronous round trip per item.
- Gradual signaling: reduce the accepted rate incrementally as pressure rises rather than snapping from fully open to fully closed, since an abrupt cutoff followed by an abrupt resume tends to create a synchronized retry spike right when the stage is least able to absorb it.
Graceful load shedding when signaling alone isn't enough
- Priority-based dropping: tag items by priority and shed low-priority items first when a stage is over threshold.
- Partial processing: skip an expensive step (say, a heavy enrichment) and mark the item for later reprocessing rather than dropping it entirely.
- Adaptive sampling or reduced depth: process a representative subset, or a reduced-fidelity version of each item, under pressure.
- Bulk rejection with a clear signal: return an explicit rejection with backoff guidance (a retry-after style response) so producers know to slow down, rather than silently dropping or timing out.
- Degraded storage or processing mode: temporarily relax strong-consistency writes to append-only or cached writes with deferred flush, trading immediate consistency for continued availability.
Applying this to a model-serving pipeline
The same four stages map naturally onto real-time feature producers feeding a model-serving cluster: ingest receives features from upstream producers, enrich joins in additional context, transform is where GPU (graphics processing unit, the hardware most inference workloads run on)-backed model servers run inference, and store persists predictions or logs them for downstream consumption. The absorb-note angles that come up specifically in this setting:
| Mitigation | Applies at | Trigger | How to test it |
|---|---|---|---|
| Shedding policy that protects high-value users and SLOs (service-level objectives, the internal latency/availability targets a team commits to) | Ingest, before requests enter the pipeline | Queue depth or latency crosses the backpressure threshold | Run a synthetic overload with a mix of high-value and low-value traffic and confirm high-value requests keep their SLO while low-value ones are the ones shed |
| Client-facing retry-after signaling | Ingest | A request is shed rather than dropped silently | Verify shed requests receive an explicit retry-after response, and that a well-behaved client backing off on that signal actually reduces load on replay |
| Rate-limiting and overload signals surfaced back to producers | Boundary between real-time feature producers and the model-serving cluster | The model-serving cluster's queue depth or latency crosses threshold | Drive a producer at increasing rate in a game day and confirm the cluster both rate-limits the producer and sends an overload signal the producer can act on, not just an opaque error |
| Adaptive batching | Transform (GPU-backed model servers) | Queue depth rises but individual requests can tolerate a small added wait | Load-test at increasing request rates and confirm GPU utilization improves as batch size grows, while the added per-request wait stays within the latency budget |
| Degraded-model mode | Transform | GPU-backed model-server saturation persists despite batching | Force sustained saturation in a test environment and confirm the pipeline falls back to a smaller or cached model rather than queuing indefinitely, and that it fails back to the full model once pressure clears |
Testing each mitigation this way, by deliberately driving the specific stage into the condition that should trigger it and checking the pipeline behaves as designed, is what turns "we built backpressure" into a verified property instead of an assumption that only gets tested during a real incident.
Trade-offs and pitfalls
- Aggressive backpressure protects the pipeline's stability but pushes retries and latency onto upstream callers; the shedding policy needs an explicit priority scheme (like protecting high-value users) or it sheds arbitrarily.
- Load shedding risks real data loss; pairing it with retry-after signaling and, where possible, marking shed items for reprocessing keeps that loss bounded and visible instead of silent.
- Adaptive batching trades latency for throughput; a batch window that's too generous can blow through the latency budget for exactly the requests you shed less aggressively.
- A degraded-model mode is only safe if its accuracy and behavior are understood ahead of time; discovering its quality gap for the first time during a live incident defeats the purpose of having a tested fallback.
- Thresholds set once at launch drift out of date as traffic and capacity change; they need the same ongoing review as any other capacity constant.
Unlock Full Question Bank
Get access to all Scalability Patterns and Techniques interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.