It was 2:17 AM on a Tuesday when our p99 origin latency graph spiked to 1.8 seconds - not because our origins were melting down, but because a single top-of-rack switch in Ashburn started dropping 0.3% of packets. Our origins were fine. Our users weren't. That's the dirty secret of tail latency at CDN scale: you don't fix p99 by making your origin faster, you fix it by not waiting around for the slow request. I've spent the last decade chasing that tail, and request hedging is the only technique that's consistently cut our p99 in half without doubling our origin bill.
Why Your p99 Has Almost Nothing to Do With Origin Performance
Most teams look at a p99 spike and immediately blame origin CPU or database contention. In my experience, that's rarely the culprit. When you're operating at the edge, tail latency is dominated by network pathologies, transient congestion, garbage collection pauses on origin hosts, and TCP retransmissions - all stuff that affects a tiny fraction of requests randomly. You can have an origin that serves 99% of requests in 40ms and still see a p99 of 800ms. It's not a capacity problem, it's a variance problem. Standard retries don't help here because by the time you've waited for a timeout, you've already blown your latency budget.
Hedging is fundamentally different from retrying. With a Origin Retry Strategies for CDN Resilience: Backoff, Jitter, and Budget Limits approach, you wait for a failure or timeout, then try again. With hedging, you don't wait. You send the same request to a second origin (or the same origin over a different path) after a short delay, and you take whichever response comes back first, canceling the other. You're racing the requests against each other. Google published the original pattern in their "The Tail at Scale" paper, and it works because the probability that *both* parallel requests hit the same transient slowdown is vanishingly small.
The trick is picking that delay - the hedge timer. Too short and you're just doubling your origin traffic for no reason. Too long and you've already missed your SLO. We learned this the hard way in 2019 when we set a 10ms hedge timer globally. Origin QPS jumped 85% and we barely moved p99 because we were hedging requests that would have completed in 25ms anyway. Your hedge timer should be tied to your origin latency distribution, not a static guess. I now set it to the p90 or p95 latency for that origin and route, so we only hedge requests that are already slower than expected.
// Hedged origin fetch with p95-based timer
// Hedging only kicks in if the primary hasn't returned
async function fetchWithHedging(request, originPool) {
const primary = originPool.selectPrimary(request);
const hedgeDelayMs = originPool.getP95LatencyMs(primary.region); // e.g., 85ms
let hedgedRequest = null;
const hedgeTimer = setTimeout(async () => {
// Only hedge if we have budget and origin is healthy
if (hedgeBudget.tryConsume() && originPool.isHealthy(primary)) {
const secondary = originPool.selectSecondary(request, primary);
hedgedRequest = fetchFromOrigin(request, secondary, {
priority: 'hedge',
// Critical: propagate hedge header so origin can deprioritize if needed
headers: { 'X-Hedge-Request': '1', 'X-Request-ID': request.id }
});
}
}, hedgeDelayMs);
try {
const primaryResponse = await fetchFromOrigin(request, primary);
clearTimeout(hedgeTimer);
if (hedgedRequest) hedgedRequest.cancel();
return primaryResponse;
} catch (err) {
clearTimeout(hedgeTimer);
// If primary fails fast, don't wait for timer - hedge immediately
if (isRetriable(err) && hedgeBudget.tryConsume()) {
const secondary = originPool.selectSecondary(request, primary);
return fetchFromOrigin(request, secondary, { headers: { 'X-Request-ID': request.id }});
}
throw err;
}
}
Building a Hedge Strategy That Won't Take Down Your Origins
Here's where most implementations go wrong: they treat hedging as free. It isn't. Every hedged request is real load on your origins, real connections from your Origin Connection Pooling Optimization: Reducing TCP Overhead at CDN Scale pools, and real work for your databases. If you hedge 20% of requests during an origin slowdown, you can easily create a retry storm that turns a minor blip into a full outage. You must have a strict hedge budget. We enforce a token-bucket budget that caps hedged requests to 10-15% of total origin traffic per PoP, per minute. When the bucket's empty, we don't hedge - we just let the primary request play out, however slow it is.
You also need to be smart about *what* you hedge. Never hedge non-idempotent requests - POSTs, PATCHes, anything that mutates state. And don't hedge cacheable GETs that can be coalesced. If 50 users request the same uncached asset at once, we collapse them into one origin fetch with request coalescing; hedging that collapsed request once is useful, hedging 50 individual requests is catastrophic. I also recommend tying your hedging logic into your health checks. If your Origin Health Monitoring Patterns: Active Probes, Passive Checks, and Hybrid Approaches system has already marked an origin as degraded, skip hedging to that origin entirely and pick a known-healthy peer in another AZ or region instead.
Cancellation is the other half of the equation that folks forget. Per the HTTP Working Group Specifications (https://httpwg.org/specs/), you should signal cancellation promptly to free up origin resources. If your edge wins the race with the secondary request, you must abort the primary TCP stream immediately - don't just ignore the response and let the origin finish rendering a 2MB JSON payload for nobody. On our Nginx and Envoy-based edges, we send a RST_STREAM (for HTTP/2 and HTTP/3) or close the connection. Without aggressive cancellation, we measured origins doing 30% wasted work during hedge-heavy periods. The web.dev Articles (https://web.dev/articles/) on fetch priority and early hints reinforce this same principle: don't hold resources for a race you've already lost.
# Envoy-style hedge policy with budget and cancellation
origin_hedge_policy:
hedge_on_per_try_timeout: true
per_try_idle_timeout: 85ms # matches p95, not a fixed 50ms guess
hedge_budget:
percent: 12.0
min_requests: 10 # allow hedging even at low QPS
retry_budget:
percent: 20.0
# Only hedge idempotent, cache-miss GETs
hedge_predicate:
- request_method: GET
- header_match: { name: ":method", value: "GET" }
- cache_state: MISS
cancellation:
propagate_cancel: true # sends RST_STREAM / connection close on winner
hedged_request_headers:
x-hedge-attempt: "true" # origin can shed load if overloaded
# Origin-side: deprioritize hedged requests under load
# In your origin app server (e.g., NGINX + Lua)
# if ngx.var.http_x_hedge_attempt == "true" and queue_depth > 100 then
# return 503 // let edge try another origin, don't queue hedged work
# end
Quantifying the Win: What Hedging Actually Costs vs. Saves
Everyone wants to know if parallel requests are worth the extra origin load. The answer is yes, but only if you measure the right things. Don't look at average latency - hedging barely moves p50, sometimes it makes it 1-2ms worse due to added contention. Look at p95, p99, and p99.9. In our production rollout across 45 PoPs, hedging cut p99 origin latency by 42-58% while increasing total origin QPS by only 8-11%. That's the trade-off you want. If your hedging is adding 30% load for a 10% p99 improvement, your timer is too aggressive or your budget is too loose.
We A/B tested hedging for three weeks on our image optimization origin fleet - one of our most variable workloads because image re-encoding times swing wildly. The control group used standard 1s timeout + 1 retry. The hedged group used an 80ms hedge delay (our p92 at the time) with a 12% budget. The hedged group not only had dramatically better tail latency, but origin error rate dropped because we weren't hammering struggling origins with retries after they'd already timed out. You can't capture that nuance if you're only watching origin CPU. You have to watch end-user TTFB and the ratio of hedged requests that actually win the race.
Here's a snapshot from that test, pulled from a 24-hour window at ~18k RPS per PoP. These numbers are typical for a well-tuned hedge strategy on a cache-miss-heavy workload. Notice how small the hit to p50 is compared to the gain at p99:
| Metric | Without Hedging (Control) | With Hedging (12% Budget, 80ms Delay) | Change |
|---|---|---|---|
| Origin p50 Latency | 42 ms | 44 ms | +4.7% |
| Origin p95 Latency | 118 ms | 89 ms | -24.6% |
| Origin p99 Latency | 642 ms | 311 ms | -51.6% |
| Total Origin QPS (per PoP) | 2,150 req/s | 2,365 req/s | +10.0% |
| Client TTFB p99 | 712 ms | 358 ms | -49.7% |
Putting Hedging Into Production: Timers, Cancellation, and Connection Reuse
Here's where most teams get hedging wrong: they treat it like a simple retry. It's not. A retry waits for failure. A hedge assumes slowness is failure and acts before your origin even tells you something's wrong. The magic is in the delay timer — we call it the hedge delay — and how aggressively you cancel the loser.
For a typical API origin with a p50 of 45ms and a p95 of 180ms, we've had great luck starting with a hedge delay at your p90 + 15ms. So if your p90 is 110ms, hedge at 125ms. That means ~10% of requests will trigger a second origin fetch, which is enough to crush your p99 without doubling your origin load. Don't just pick 50ms because it sounds fast; you'll hedge 40-50% of traffic and melt your origin pool during peak. You'll want to tune this alongside your Origin Timeout Configuration Guide: Connect, Read, and Total Timeout Tuning — if your read timeout is 2s, a 125ms hedge gives you plenty of room to race and cancel before you hit the hard timeout.
The second non-negotiable is cancellation. If your edge doesn't send an RST or HTTP/2 RST_STREAM to the losing origin connection the moment the winner returns, you haven't built hedging, you've built a DDoS tool. Reuse that TCP connection and TLS session — don't close it — but abort the in-flight request cleanly.
// Go-style hedged origin fetch with p90-based delay and loser cancellation
func FetchWithHedge(ctx context.Context, req *http.Request) (*http.Response, error) {
hedgeDelay := 125 * time.Millisecond // p90 (110ms) + 15ms buffer
ctx, cancel := context.WithCancel(ctx)
defer cancel()
type result struct { resp *http.Response; err error }
ch := make(chan result, 2)
// Primary request
go func() {
resp, err := originClient.Do(req.WithContext(ctx))
ch <- result{resp, err}
}()
// Hedged request - only fires if primary is slow
timer := time.NewTimer(hedgeDelay)
defer timer.Stop()
go func() {
select {
case <-timer.C:
// Clone request for hedge - must be idempotent (GET/HEAD)
hedgeReq := req.Clone(ctx)
resp, err := originClient.Do(hedgeReq)
ch <- result{resp, err}
case <-ctx.Done():
return // Primary won, don't hedge
}
}()
// First response wins, cancel the other
res := <-ch
cancel() // Sends RST_STREAM / abort to loser
return res.resp, res.err
}
Advanced Patterns: When Hedging Bites Back
So you've shipped hedging and your p99 dropped from 850ms to 210ms. Nice. Then Black Friday hits and your p99 is worse than before. What happened? You probably hedged non-idempotent requests or ignored your retry budget. Never hedge POSTs, PATCHes, or anything that isn't safely replayable unless you've got request de-duplication at the origin. We've seen a single hedged POST create duplicate orders because the first request wasn't actually slow — it was just processing — and both eventually committed. Stick to GET and HEAD, and if you must hedge other methods, require an idempotency key.
The other killer is origin connection dynamics. Hedging interacts weirdly with TLS and keepalives. If you're hedging across two different origin IPs or PoPs, that second request needs a full TLS handshake — that's an extra 30-80ms of TCP + TLS — so your 125ms hedge might not actually win. We keep hedging inside the same edge's warm connection pool where we already have an established, verified session. That also avoids tripping any upstream WAF or rate-limiting. Speaking of which, if your edge fleet is NATed behind a small set of egress IPs, aggressive hedging looks exactly like a burst attack. Keep an eye on your Origin IP Reputation Management: Keeping Your CDN Source IPs Clean and talk to your origin team about allowlisting your hedge burst factor. A 10% hedge rate is invisible; a 40% rate will get you throttled.
Finally, implement a global hedge budget. Don't let hedging run wild when origins are genuinely struggling. We use a simple token bucket per PoP: allow hedging for at most 15% of requests over a 10s window, and if the origin's 5xx rate crosses 2%, cut hedging to 5% or disable it entirely. And remember, hedging doesn't excuse sloppy origin TLS — if that second connection is racing, you still need full verification on both. Skipping it for speed is how you get owned, so keep your Origin SSL Verification at CDN Edges: Preventing Man-in-the-Middle Attacks strict on every hedged socket, not just the primary.
Frequently Asked Questions
Won't hedging just double my origin QPS and cost?
No, if you've tuned the delay correctly. With a p90+15ms hedge delay, you're only forking 8-12% of requests. Because you cancel the loser immediately after the winner returns — typically within 20-30ms — the actual extra CPU time on origin is closer to 3-5%. That's cheap compared to shaving 400-600ms off your p99. If you're seeing >25% hedge rate, your delay is too aggressive.
Should I hedge cache MISSes, revalidations, or both?
Start with cache MISSes only. They're already on the slow path and they benefit the most. Hedging stale-while-revalidate or If-Modified-Since revalidations can help, but it gets noisy — a 304 is cheap to serve anyway, so the win is smaller. We've measured a 60% p99 improvement for MISS hedging vs. only ~15% for revalidation hedging. Nail MISSes first.
What's the difference between hedging and a normal retry with a short timeout?
A retry is sequential: you wait for a timeout or error, then try again. You've already paid the full timeout penalty. Hedging is parallel: you send the second request before* the first has failed, at say 125ms, and race them. For tail latency caused by slow-but-not-failed origins — which is 90% of p99 issues — hedging cuts latency by 50-70% where retries do almost nothing.
How do I know if hedging is actually helping?
Don't look at averages. Graph p95, p99, and p99.9 with hedging on vs. off per PoP, plus your origin request count and 5xx rate. A healthy hedge deployment shows p99 down 40-60%, p95 nearly flat, origin load up 5-10%, and no increase in 5xx. If p99 doesn't move but load spikes, your delay is wrong or your origin is uniformly slow — hedging can't fix a consistently slow origin, only variance.