The Universal Scalability Law and the coherence term
What it is
A model of how throughput scales with concurrency, and it has three terms rather than the two most people carry.
$$ C(N) = \frac{N}{1 + \alpha(N-1) + \beta N(N-1)} $$
N concurrency: nodes, threads, or users
alpha CONTENTION: serialised work. Amdahl's term.
beta COHERENCE: the cost of keeping N things
consistent with each other. Quadratic.
C(N) relative capacity against a single unit
The third term is the whole point. With contention alone (Amdahl's law), throughput plateaus: adding capacity stops helping and does no harm. With the coherence term, throughput reaches a maximum and then declines: adding capacity makes the system slower.
LINEAR C(N) = N the fantasy
AMDAHL alpha > 0, beta = 0 plateaus
USL alpha > 0, beta > 0 PEAKS, then falls
Commonly confused with Amdahl's law. Amdahl says a serial fraction caps your speedup. USL says that beyond a point, more concurrency is actively worse, and that retrograde region is the thing production systems actually hit.
The problem it solves
It explains the observation that adding capacity made things worse, which the plateau model says is impossible.
Adding pods to a service and seeing throughput DROP.
Adding threads to a pool and seeing p99 rise.
Adding nodes to a cluster and watching total throughput
peak at 12 nodes and decline at 16.
Under Amdahl, none of these can happen. Under USL they are
the expected behaviour past N*, and knowing that changes
the response from "add more" to "find the coherence
source".
And it gives you a number to design toward: the peak concurrency, past which you must reduce coherence rather than add capacity.
Mechanics
What each term physically is
ALPHA: CONTENTION (serialisation)
Work that cannot proceed in parallel because something
is exclusive.
a global lock
a single-writer database
a shared queue with one consumer
a mutex around a hot data structure
connection pool exhaustion
Cost grows LINEARLY with N: each additional participant
waits behind the same serial section.
BETA: COHERENCE (crosstalk)
Work required to keep N participants CONSISTENT with each
other, which is pairwise and therefore quadratic.
cache-line invalidation across cores
distributed cache coherency traffic
gossip protocols
a consensus group's message count
replication fan-out
a shared counter every node updates
Cost grows as N(N-1): every participant must reconcile
with every other.
The distinction that matters practically: contention is a queue, coherence is a conversation. You fix contention by removing the exclusive resource; you fix coherence by removing the need for participants to agree.
The peak, which is the number to know
Differentiating and solving gives the concurrency at which throughput is maximised:
$$ N^* = \sqrt{\frac{1 - \alpha}{\beta}} $$
WORKED
alpha = 0.03 (3% serialised)
beta = 0.0001
N* = sqrt(0.97 / 0.0001) = sqrt(9700) ≈ 98
So throughput peaks at about 98 concurrent units and
DECLINES after that.
C(98) = 98 / (1 + 0.03(97) + 0.0001 x 98 x 97)
= 98 / (1 + 2.91 + 0.951) = 98 / 4.86 ≈ 20.2
C(200) = 200 / (1 + 5.97 + 3.98) = 200 / 10.95 ≈ 18.3
*** Doubling from 98 to 200 units REDUCES throughput by
about 9 percent, and doubles the cost. ***
And the sensitivity worth noticing: beta dominates. Halving alpha from 0.03 to 0.015 moves N* from 98 to 99. Halving beta from 0.0001 to 0.00005 moves it to 139. So the coherence term is where the scalability work is, and teams reliably attack contention because locks are visible and crosstalk is not.
Fitting it to your system
The model is only useful if you measure your own coefficients, and that is a load test rather than a guess.
1. Measure throughput at several concurrency levels.
N = 1, 2, 4, 8, 16, 32, 64, ...
Enough points on both sides of the suspected peak.
2. Compute relative capacity C(N) = X(N) / X(1).
3. Fit alpha and beta by least squares.
from scipy.optimize import curve_fit
import numpy as np
def usl(n, alpha, beta):
return n / (1 + alpha * (n - 1) + beta * n * (n - 1))
# throughput measured at each concurrency level
n = np.array([1, 2, 4, 8, 16, 32, 64, 128])
x = np.array([100, 195, 370, 680, 1150, 1700, 1900, 1750])
c = x / x[0]
(alpha, beta), _ = curve_fit(usl, n, c, bounds=([0, 0], [1, 1]))
n_star = np.sqrt((1 - alpha) / beta)
print(f"alpha={alpha:.4f} beta={beta:.6f} N*={n_star:.0f}")
Notice the data: throughput rises to 1,900 at N=64 and falls to 1,750 at N=128. That downturn is the retrograde region, and its presence in the measurements is what tells you beta is non-zero. A load test that stops at the plateau never sees it, which is why load tests should deliberately push past the peak.
What each coefficient tells you to do
HIGH ALPHA (contention), low beta
The system serialises. Find the exclusive resource.
profile for lock wait time
check connection pool saturation
look for a single-writer bottleneck
check for a shared queue with one consumer
Fixes: shard the lock, partition the data, add writers,
make the critical section shorter.
HIGH BETA (coherence)
Participants are talking to each other, quadratically.
a shared counter or cache every node updates
cache invalidation broadcast
gossip or heartbeat traffic scaling with N
a distributed lock or consensus on the hot path
false sharing across cache lines
Fixes: partition so participants do not need to agree,
make state local and reconcile asynchronously, use
CRDTs so agreement is unnecessary, reduce the fan-out.
BOTH LOW
You are scaling nearly linearly, which usually means the
work is genuinely independent. Good, and check that the
load test is actually loading the right thing.
The single most common coherence source in application code is a shared mutable counter or cache that every instance updates, and the fix is almost always to make it per-instance and aggregate asynchronously, which trades exactness for scalability.
Where it shows up
CPU CORES false sharing: two variables on one cache
line, written by two cores, force
invalidation traffic. Padding fixes it and
the effect is dramatic.
THREAD POOLS more threads past the peak increases
context switching and lock contention
without increasing throughput. This is why
"add threads" often makes p99 worse.
DATABASE adding connections past the pool's optimum
CONNECTIONS increases contention on shared structures.
Postgres's guidance of a small pool
relative to core count is USL in practice.
CLUSTER NODES gossip and replication traffic scales with
N, so a cluster has an optimum size for a
given workload.
CONSENSUS message count grows with group size, which
GROUPS is why Raft groups are 3 or 5 and not 11.
The Raft example is a good concrete anchor: five nodes rather than eleven is a USL decision, and the cost of the eleventh is coherence rather than contention.
A worked example: adding capacity made it slower
SYMPTOM
A service scaled from 8 to 24 pods during a traffic
increase. Throughput rose to 16 pods and then fell.
At 24 pods, total throughput was 6% BELOW the 16-pod
figure and p99 was 40% worse.
THE LOAD TEST
Ran a controlled test at 1, 2, 4, 8, 12, 16, 20, 24, 32
pods, deliberately going past the observed peak.
N throughput (rps) C(N)
1 420 1.00
2 810 1.93
4 1,560 3.71
8 2,840 6.76
12 3,610 8.60
16 4,010 9.55
20 4,050 9.64
24 3,780 9.00
32 3,120 7.43
Fitted: alpha = 0.041, beta = 0.00095
N* = sqrt(0.959 / 0.00095) ≈ 32... but the measured peak
is at 20.
The discrepancy is itself informative: the fit is poor
above 24 because a SECOND mechanism kicks in there. That
turned out to be connection pool exhaustion at the
database, which is contention rather than coherence, so
the single-model fit was blending two regimes.
FINDING THE COHERENCE SOURCE
beta = 0.00095 is high. What is quadratic?
Each pod maintained an in-memory cache of feature flags
and refreshed it by SUBSCRIBING to a Redis pub/sub
channel. Every flag change fanned out to every pod, and
every pod wrote a heartbeat to a shared Redis key that
every other pod read to build a peer list for a
client-side load balancer.
-> The peer-list heartbeat was O(N^2) message volume.
At 24 pods that was 552 reads per interval; at 8 pods
it was 56.
THE FIX
a. Replaced the peer-list gossip with the platform's
service discovery, which is a single lookup rather
than an all-pairs exchange.
-> beta 0.00095 -> 0.00021
b. Feature flag refresh moved from pub/sub fan-out to a
polled ETag-conditional fetch, so a flag change costs
N cheap requests rather than N persistent
subscriptions.
c. Separately, raised the connection pool ceiling and
added a pgbouncer layer, which addressed the
contention regime above 24.
REFIT: alpha = 0.022, beta = 0.00021
N* = sqrt(0.978 / 0.00021) ≈ 68
RESULT
Throughput at 24 pods rose 34%, and the system now scales
usefully to about 64 pods instead of peaking at 20.
THE LESSON
The instinct was "add pods". The measurement said the
peak was at 20 and we were past it. And the coherence
source was a peer-list heartbeat nobody thought of as
part of the request path at all.
Production evidence
Neil Gunther's Guerrilla Capacity Planning introduces the USL and the fitting methodology, and his later work reframes the coefficients as contention and coherence with physical interpretations rather than as fitting parameters.
Amdahl's law (1967) is the special case with beta = 0, and the comparison is the clearest way to see what the coherence term adds: Amdahl predicts a plateau, and real systems exhibit a peak.
Postgres connection-pool guidance recommending a pool size closer to the core count than to the client count is USL in practice: past an optimum, more connections increase contention on shared structures and reduce throughput, which is why pgbouncer exists.
Raft and Paxos group sizing at 3 or 5 rather than larger is a coherence decision: message count grows with group size, so the marginal availability of a seventh node is outweighed by the coordination cost.
False sharing in multicore programming is the hardware-level instance of the coherence term, and the standard fix (padding to cache-line boundaries) produces the dramatic improvements documented across the concurrency literature, which makes it the clearest demonstration that beta is physical rather than statistical.
The debate
The case for using USL: it is the only common model that predicts the retrograde region, and that region is what production systems actually hit. It gives you a number, N*, and it distinguishes two failure modes with different fixes.
The case against: it is a two-parameter curve fit to a system with many mechanisms, so the coefficients are descriptive rather than causal, and a poor fit is common when several bottlenecks operate in different regimes. Treating alpha and beta as physical quantities can mislead.
The case for just load testing: measure throughput at several concurrency levels and read the peak off the graph. You get the actionable number without the model.
My position: fit the model to get the two coefficients, because their ratio tells you which class of fix to pursue, and treat N as approximate.*
The actionable output is not N* itself, which a load test gives you directly. It is whether alpha or beta dominates, because those need completely different work: contention means find the exclusive resource and shard it; coherence means remove the need for participants to agree. Reading a peak off a graph tells you where you are and not what to do.
And the sensitivity argument makes that concrete: halving alpha barely moved N in the example, while halving beta moved it from 98 to 139.* Coherence dominates the scalability ceiling, and teams reliably attack contention instead because locks are visible in a profiler and crosstalk is not.
The methodological point I would insist on is that the load test must go past the peak. A test that stops at the plateau never observes the downturn, so beta cannot be fitted and the retrograde region is invisible until production finds it. Most load tests stop when throughput stops rising, which is exactly one measurement too early.
Where I would be careful, and say so: a poor fit is informative rather than a failure. In the worked example the model diverged above 24 pods because a second mechanism, connection pool exhaustion, took over, and the single fit was blending two regimes. Noticing that the curve stops matching is how you find the second bottleneck, so I would plot the residuals rather than just reporting the coefficients.
Follow-up Q&A
"What does the Universal Scalability Law say that Amdahl's law does not?" That throughput can decline. Amdahl has one term, contention, so the prediction is a plateau: adding capacity stops helping and does no harm. USL adds a coherence term that is quadratic in N, so throughput reaches a maximum and then falls. That retrograde region is what production systems actually hit, and under Amdahl it is impossible, so a team using the plateau model responds to a slowdown by adding more.
"What physically is the coherence term?" The cost of keeping N participants consistent with each other, which is pairwise and therefore quadratic. Cache-line invalidation between cores, gossip and heartbeat traffic, replication fan-out, a distributed lock on the hot path, a shared counter every node updates. Contention is a queue behind an exclusive resource; coherence is a conversation between participants. They need different fixes, which is why separating them matters.
"How do you get the coefficients?" A load test at several concurrency levels, then a least-squares fit of the two-parameter curve to the relative throughput. And the test has to go past the peak, because a test that stops when throughput stops rising never observes the downturn, so beta cannot be fitted. Most load tests stop exactly one measurement too early.
"Which coefficient should you attack?" Beta, almost always, and it is the counter-intuitive part. In one worked case, halving alpha moved the peak concurrency from 98 to 99 and halving beta moved it from 98 to 139. Coherence dominates the ceiling. But teams reliably attack contention instead, because a lock shows up in a profiler and crosstalk between instances does not.
"What does a high beta look like in application code?" Usually a shared mutable thing every instance touches. A counter or cache that all nodes update, a cache-invalidation broadcast, gossip or heartbeat traffic that scales with the number of participants, or a peer list every node maintains by talking to every other node. The fix is generally to make the state local and reconcile asynchronously, or use a data type that converges without agreement, which trades exactness for scalability.
"Walk me through a case." A service scaled from 8 to 24 pods and throughput fell six percent below the 16-pod figure. A load test at nine concurrency levels showed the peak was at 20 pods. Fitting gave a high beta, so the question became what was quadratic, and it turned out each pod wrote a heartbeat to a shared key that every other pod read to build a peer list for client-side load balancing: 552 reads per interval at 24 pods against 56 at 8. Replacing that with service discovery took beta from 0.00095 to 0.00021 and moved the useful ceiling from about 20 pods to about 64.
"What if the model fits badly?" That is informative rather than a failure. In the same case the fit diverged above 24 pods because a second mechanism, connection pool exhaustion at the database, took over, so the single two-parameter fit was blending two regimes. Noticing that the curve stops matching is how you find the second bottleneck, which is why I would plot the residuals rather than just report alpha and beta.
"Where does this show up outside distributed systems?" At every scale. False sharing between CPU cores, where two variables on one cache line written by two cores force invalidation traffic, is the hardware instance and padding fixes it dramatically. Thread pools, where adding threads past the optimum increases context switching without increasing throughput, which is why "add threads" often makes p99 worse. Postgres connection pools, which is why pgbouncer exists. And Raft group sizing at three or five rather than eleven, which is a coherence decision.
"Isn't a load test enough?" For the number, yes: you can read the peak off the graph. What the fit adds is which class of fix to pursue, because the ratio of alpha to beta tells you whether to hunt an exclusive resource or a conversation between participants, and those are completely different pieces of work. A peak tells you where you are; the coefficients tell you what to do.
Common misconceptions
"Adding capacity can't make it slower." That is Amdahl's prediction. With a non-zero coherence term, throughput peaks and then declines.
"USL is just Amdahl with extra maths." The extra term changes the qualitative prediction from a plateau to a peak, which is the difference between "stop adding" and "you are past the optimum".
"Attack the locks." Contention is visible and usually smaller. Coherence dominates the ceiling and is invisible in a profiler.
"The coefficients are physical constants." They are a two-parameter fit to a system with several mechanisms. Use them to classify, not to predict precisely.
"Load test until throughput plateaus." Stop there and you never see the retrograde region, so you cannot fit beta and production finds it for you.
Interview delivery note
Lead with what it predicts that the familiar model does not: "Amdahl says a serial fraction caps your speedup, so throughput plateaus. USL adds a coherence term that's quadratic in N, so throughput peaks and then declines. That retrograde region is what production systems actually hit, and under the plateau model it's impossible, so a team seeing a slowdown responds by adding more capacity."
Separate the two terms physically: "Contention is a queue behind an exclusive resource: a lock, a single writer, a pool. Coherence is a conversation between participants: cache invalidation, gossip, a shared counter every node updates. It's quadratic because it's pairwise, and the two need completely different fixes."
Give the sensitivity, because it is the counter-intuitive and actionable part: "And beta dominates. In one case halving alpha moved the peak concurrency from 98 to 99, and halving beta moved it from 98 to 139. So coherence is where the scalability work is, and teams reliably attack contention instead because a lock shows up in a profiler and crosstalk between instances doesn't."
Tell the case concretely: "A service went from eight to twenty-four pods and throughput dropped six percent. The load test showed the peak was at twenty. The high beta turned out to be a peer-list heartbeat: every pod wrote to a shared key every other pod read, so five hundred and fifty-two reads per interval at twenty-four pods against fifty-six at eight. Replacing it with service discovery moved the useful ceiling from twenty pods to about sixty-four."
Close on the methodological point: "and the load test has to go past the peak. Most stop when throughput stops rising, which is exactly one measurement too early, so beta never gets fitted and the retrograde region is invisible until production finds it."
Further reading
- Neil Gunther, Guerrilla Capacity Planning, for the model, the fitting methodology and the contention/coherence interpretation.
- Amdahl, "Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities" (1967), as the beta-equals-zero special case.
- The PostgreSQL wiki on connection pooling and the pgbouncer documentation, for USL in a system people actually operate.
- Any treatment of false sharing in multicore programming, as the hardware-level demonstration that the coherence term is physical.