You must choose the permit count for a semaphore that bounds in-flight work against a shared dependency. Walk through the reasoning you would use to derive a number rather than guess one, and how you would revisit it as conditions change.
answer
- L = λ × W — permits = throughput × hold time
- latency ∝ 1/(1−ρ): target 0.6–0.8 utilisation
- find the knee by ramping concurrency
- permits × replicas = what the dependency sees
- cap hold time too; the number is a hypothesis to re-derive
basics
~20 sStart from Little's law: in-flight = throughput x latency. Pick the throughput you must sustain, multiply by measured service time, then check the number against the dependency's own capacity budget divided across replicas, and keep utilisation below saturation. Then measure and adjust.
solid answer
~60 sDerive it, don't guess it. **Little's law** gives the anchor: `L = λ × W`, where L is the mean number in flight, λ the arrival/throughput rate and W the mean time each unit occupies a permit. Want 500 requests/s against a dependency with a 40 ms mean service time? `L = 500 × 0.04 = 20` permits to sustain that rate at all. Then apply three corrections. **Queueing theory**: latency rises hyperbolically as utilisation approaches one, so size for a target utilisation around 0.6–0.8 of the dependency's real ceiling, not 1.0. **The budget split**: the number that matters to the dependency is `permits × replicas`, so start from its total safe concurrency and divide. **Variance**: mean W understates the tail; check with the p95 service time and decide whether you would rather queue or shed. Finally, the permit count is a *protection* level, not a throughput target — it should be low enough that the dependency degrades gracefully, and paired with a bounded wait so excess load is shed rather than queued. Re-derive it whenever measured W or the replica count moves; instrument utilisation, wait time and rejections so the number stays evidence-backed.
code
text · 15 linestarget throughput λ = 500 req/s
measured hold time W = 40 ms (p50), 180 ms (p95) -> use both
Little's law floor: L = 500 * 0.040 = 20 permits
sanity vs p95: 500 * 0.180 = 90 permits to hold the rate
in the tail -> you will shed in the tail, by design
dependency's measured knee (load test): C = 120 concurrent
our negotiated share: 60
replicas: 6
permits per process: 60 / 6 = 10
chosen: 10 per process (fleet 60, below the 120 knee, ~50% utilisation)
consequence: sustainable ~ 10/0.040 = 250 req/s per replica; 1500 fleet-wide
policy at the cap: try_acquire(50ms) then rejectgo deeper
Know the relationship in-flight = rate × time, and that the number must come from a measurement of how long each unit holds a permit.
Apply Little's law to a concrete target, and explain why headroom below full utilisation is required because latency blows up as utilisation approaches one.
Add the empirical knee from a load test, cap the hold time, choose the overload policy, and instrument utilisation, waiters and rejections so the number can be revisited.
Own the capacity contract end to end: the dependency's total safe concurrency, how it is divided across services and replicas, what autoscaling does to it, whether an adaptive limit is warranted, and the review trigger that makes the constant a living decision.
## Framing: what the number means A permit count is a statement about *concurrency* — how many units of work may occupy the dependency at once. It is not a rate, not a queue length and not a thread count. Getting the number right is a small piece of applied queueing theory plus a capacity negotiation with whatever sits downstream. ## Step 1 — Little's law gives the floor Little's law holds for any stable system regardless of arrival distribution or service discipline: `L = λ × W` - **L** — mean number of units in the system (here: permits held). - **λ** — mean arrival/completion rate. - **W** — mean time a unit spends in the system (the whole time a permit is held, including network, queueing inside the dependency and deserialisation — not just the dependency's internal service time). Read it two ways. Forwards: to sustain 500 requests/s where each holds a permit for 40 ms, you need at least `500 × 0.04 = 20` permits; with fewer, arrivals exceed departures and the queue grows without bound. Backwards: given N permits and a measured W, the ceiling on throughput is `N / W`, regardless of how many callers you add. That backwards reading is the honest way to tell a product team what a limit costs them. The most common mistake is measuring W as the dependency's reported service time rather than the full permit-hold time. Every millisecond between acquire and release counts, including retries and connection setup. ## Step 2 — Queueing theory says don't size for full utilisation For a simple M/M/1-ish model, mean waiting time scales as `W ∝ 1/(1 − ρ)` where ρ is utilisation. At ρ = 0.5 latency is roughly double the service time; at 0.9 it is ten times; at 0.99 it is a hundred. Real systems are worse than the model because of variance and coordinated arrivals. Consequences: - Size for a target utilisation of roughly 0.6–0.8 of the dependency's measured saturation point, not 1.0. The last 20% of capacity costs unbounded latency. - With **c** parallel servers behind the dependency (M/M/c), pooling helps: c servers at utilisation ρ queue far less than c independent single-server queues, which is an argument for one shared limiter over many small per-caller ones. ## Step 3 — Find the dependency's real ceiling empirically The theory tells you the shape; only a load test tells you the constant. Ramp concurrency and plot throughput and p99 latency against it. Throughput climbs, then flattens, then latency knees upward while throughput is flat or falling — that knee is the dependency's usable concurrency. Beyond it you are adding queueing, not work; past it many systems collapse (thrashing, GC pressure, connection exhaustion, retry storms). Set the global budget at or below the knee. ## Step 4 — Split the budget across replicas and callers A process-local semaphore caps one process. What the dependency experiences is `permits_per_process × replicas × callers`. So the derivation runs downwards: total safe concurrency C at the dependency → your service's share (negotiated, since other clients exist) → divided by your replica count → permits per process. This makes the number fragile in exactly one direction: autoscaling. Double the replicas to handle load and you double the pressure on the dependency that was already the bottleneck. Options: re-derive permits as `share / replicas` dynamically from the actual replica count; keep per-replica permits low enough that the maximum fleet size is still safe; or move the limit to a shared coordination point and accept the extra latency and failure mode. Adaptive limiters (which infer the concurrency limit from observed latency gradients, the way congestion control infers bandwidth) are the third option and are worth naming. ## Step 5 — Decide what happens at the boundary The permit count only defines the cap; the policy defines the behaviour at the cap. Choose deliberately: - **Shed** — bounded acquire, then reject. Preserves latency for admitted work; the caller sees a clear signal. - **Queue** — allow waiting, bounded in both count and time. Smooths bursts, but every queued millisecond is latency the client pays. - **Degrade** — serve a cached or partial result rather than reject. Under overload, LIFO or deadline-aware admission beats FIFO, because FIFO preferentially serves requests whose callers have already timed out. ## Step 6 — Variance, not just means Little's law is about means. If W has a heavy tail — an occasional 2-second call among 40 ms ones — then a permit count sized on the mean will be fully occupied by tail calls exactly when load is highest. Sanity-check the number against p95 W, and cap the hold time itself with a call timeout so a single stuck call cannot occupy a permit indefinitely. A permit held forever is a permanent reduction in your limit. ## Step 7 — Instrument and re-derive The number is a hypothesis with a shelf life. Export permits in use, waiters, wait time, rejections and the measured W; alert when utilisation sits above target or when rejections appear without a corresponding downstream problem. Re-derive after any change to W (a downstream deployment, an added network hop, a schema change) or to replica count. Treating the constant as permanent is the actual failure — the derivation, the measurements and the review cadence are the deliverable, not the integer. ## What a strong answer sounds like A number, the law it came from, the measurement it depends on, the utilisation headroom, the fleet-wide multiplication, the overload policy, and the trigger that would make you change it.
- Your derivation assumed a fixed replica count and the service now autoscales. What breaks?The dependency sees permits multiplied by replicas, so scaling out to absorb load also scales the pressure on the bottleneck you were protecting — the limiter stops limiting exactly when it matters. Fixes are to compute permits from the live replica count so the fleet total stays fixed, to size per-replica permits against the maximum fleet size, or to move the budget to a shared limiter and accept its latency and availability cost.
- Downstream latency doubles during an incident. What does your limiter do, and is that the behaviour you want?With permits fixed, doubling the hold time halves the throughput the limiter can pass, so it self-throttles and starts rejecting. That is usually the desired behaviour: it keeps concurrency at the dependency constant while the dependency is struggling, instead of piling on. What you must avoid is a knee-jerk permit increase during the incident, which converts a slow dependency into a dead one.
- When would you prefer an adaptive concurrency limit over a fixed permit count?When the dependency's safe concurrency is not stable — shared multi-tenant infrastructure, variable payload cost, or a system whose capacity changes with its own deploys. An adaptive limiter infers the limit from observed latency or queueing gradients, much as congestion control infers available bandwidth, and adjusts continuously. The trade is complexity and the risk of oscillation, so it earns its place only when a static number is provably wrong often enough to hurt.
Sizing checkout lanes in a shop. Little's law tells you how many lanes it takes to clear the expected shopper rate; queueing theory tells you why running every lane at 99% busy makes the queue explode; the fleet split is remembering that the chain has ten branches drawing on one warehouse.
saying these in an interview costs you the question
- Picking a round number like 100 with no measurement behind it
- Sizing for 100% utilisation of the dependency's measured capacity
- Using the dependency's internal service time instead of the full permit-hold time
- Ignoring that the fleet-wide limit is per-replica permits times replicas
- Raising the limit during an incident because requests are being rejected