Who Gets Dropped: Hodor and Overload Protection at LinkedIn
LinkedIn's overload shield, Hodor, runs on more than 1,000 Java microservices with zero per-service tuning — and its first load shedder, which capped how many requests a service handled at once, dropped a member's page load as readily as an offline batch job's bulk read. The framework's most important stated goal is a net positive impact on members, so priority had to be built in: traffic tiering stamps every request as optional, degradable, or non-degradable at the edge of the data center, carries the tier through the whole chain of downstream calls, and drops the lowest tier first, walking upward only if pressure persists. Sensing happens from inside the process — a heartbeat thread that notices when it can't get CPU time, a garbage-collection overhead monitor, and a thread-queue wait-time monitor, all filtered by a latency check that suppresses false alarms. Integrating priority broke the load metric itself: dropped requests contribute nothing to a concurrency count, so the shedder moved to a rate-based limit with per-tier traffic histograms. Hodor has prevented hundreds of overloads in LinkedIn production.
Drive a mixed stream of member, degradable, and offline traffic past capacity under the v1 blind concurrency shedder and watch member requests get refused while batch reads sail through — then switch on traffic tiering and watch optional traffic absorb the whole cut. Escalate until the ladder climbs, let the adaptive limit probe its way back up and burn a probe, then overload the whole cluster instead of one host and learn what the retry budget does when rescue stops being possible.
Problem
LinkedIn launched over eighteen years before these posts as a single monolithic Java web application; by the time Hodor was built, the site ran on well over 1,000 separate microservices on the JVM — the Java Virtual Machine, the runtime all of them share — working in tandem toward a 99.9% availability goal. At that scale, overload is a routine failure mode: a service pushed past its capacity stops serving traffic at reasonable latency, and left to its own devices the condition can spread until it threatens the whole site. LinkedIn had used the standard mitigations for years, and the 2022 post prices each one honestly: overprovisioning handles the expected peak but raises hardware costs; circuit breakers protect the client from a struggling downstream but do nothing for the service actually under load; and capacity numbers approximated by pushing a service to its bending point are rough, static guides that can't account for unexpected failures. What LinkedIn did not have was a standard server-side answer — a way for any service, under any overload, to protect itself.
The constraint that shapes everything is heterogeneity at fleet scale. The services have disparate workloads, execution models, and traffic patterns, so the framework had to detect a wide range of overload types: physical resource exhaustion — CPU, memory, network and disk I/O — and virtual resource exhaustion, such as execution threads, pooled database connections, and semaphore permits. The virtual kind can arrive without any increase in incoming traffic at all, because a slow downstream raises the number of requests concurrently in flight upstream. And whatever the mechanism, it had to work out of the box: one of Hodor's four stated goals is that no service owner or SRE ever tunes the algorithm. A protection scheme that needs per-service calibration will not cover a thousand services; the fleet-wide default is the whole point — and it is also the trap.
The trap is stated in the framework's own goal ranking. The fourth goal — 'the most important goal' — is that Hodor produce a net positive member impact: it exists to improve the member experience, and a net negative member experience caused by Hodor is unacceptable. But load shedding is refusal, and a shedder that runs everywhere by default, with no tuning and no knowledge of what it is refusing, has two ways to hurt the members it protects. It can fire when nothing is wrong — which is why the detectors optimize for precision over recall, and why Hodor's own overhead must be negligible, since it runs on every service at all times. And it can drop the wrong traffic: LinkedIn's services serve members actively using the platform alongside offline systems harvesting data, and the post is blunt that requests from members are more important than requests from offline jobs. A concurrency cap that rejects overflow indiscriminately spends its protection on exactly the requests the whole framework exists to keep whole.
Solution
Hodor's sensing is built on one transferable idea: measure overload from inside the process, as the application experiences it, rather than from outside it. The CPU detector is a background heartbeat thread that sleeps for a fixed interval and, on waking, compares how long it actually slept with how long it asked to sleep. The 2022 post gives a concrete example of the shape: sleep for 10ms; if the 99th percentile of a second's worth of samples exceeds 55ms, that window is in violation; eight consecutive violating windows mean the service is CPU-overloaded — thresholds arrived at through synthetic testing and production data. Measuring the ability to get CPU time inside the JVM, instead of watching OS-level CPU percentages, makes the signal architecture-agnostic — nothing changes moving between bare metal, containers, and VMs — and it sees what OS metrics miss: safepoint pauses, the moments the JVM stops all application work for garbage collection, programmatic thread dumps, or revoking locks, which hurt requests without necessarily showing in CPU graphs.
Two more detectors extend the net. The garbage-collection detector computes, on each GC event, an amortized percentage of time spent in GC — the GC overhead — and divides the percentage range into tiers, each with a violation period that shrinks as the tier rises: the more severe the GC activity, the less time Hodor waits before calling it an overload. The threadpool detector covers the virtual side, and its design choice is the post's sharpest lesson in metric selection. Backpressure — a slow downstream causing requests to accumulate upstream — typically exhausts a virtual resource like a threadpool before any physical limit is hit, and the obvious signal is queue length. The team dismissed it: queue length is difficult to correlate with any business-impact indicator, and the length at which a given service is in trouble varies with its design. Queue wait time survived instead — experimentation showed a wait-time inflection point past which a service does not return — which means an upstream service can sense a downstream's distress from its own queue, knowing nothing about the downstream at all, and shed at the point where relief actually helps. The post generalizes the idea explicitly: observing wait times to detect virtual resource overloads applies widely.
Behind the detectors sits a filter that encodes the framework's epistemics. Latency was never viable as a primary detector — not every latency increase is an overload — but every physical overload eventually surfaces as a latency increase, so latency works as confirmation. After the broad rollout, services with suboptimally tuned garbage collection produced micro stop-the-world pauses that starved the heartbeat thread and fired CPU overloads with no discernible impact on business metrics. The latency confirmation filter suppresses those: two sliding windows of very different sizes compute a slow and a fast moving average of latency, and a firing only counts when the fast average crosses the slow one shifted up by a factor. The post is candid that the crossover algorithm is far from perfect as standalone anomaly detection; as a confirmation gate on the other detectors, it is exactly enough.
Remediation began as an adaptive concurrency limit — and the adaptivity, not the limit, is the mechanism. When a detector fires, the shedder derives an initial cap on concurrent requests from the concurrency levels observed leading up to the overload, holds the service at an equilibrium it can survive, then periodically probes upward to see whether more traffic fits. A probe that re-fires the detectors is rolled back, and an exponential backoff stretches the time until the next attempt. The limit is deliberately recalculated at every new overload rather than remembered, because the last incident's number describes the last incident: the traffic pattern may differ — a surge in organic traffic one time, an offline or nearline job arriving the next — and so may the nature of the overload, since backpressure from a downstream changes capacity differently than additional organic traffic does. Concurrency beat percentage-based throttling in dark canary testing — parallel copies of services fed real production traffic — for a structural reason: a concurrency cap self-adjusts as traffic moves, automatically dropping more when traffic rises and nothing once it falls back under the limit, where a fixed percentage over-drops or under-drops.
Traffic tiering is where the shedder learns who it is dropping, and it is the piece the crux turns on. Every request carries one of three tiers: optional — droppable with no user impact at all, where most offline and nearline traffic lands; degradable — minimal user impact, the tier product owners assign to degradable features on a page, and the promotion path for offline traffic that is time-sensitive or business-critical; and non-degradable — online member requests that cannot be degraded. The numeric tier IDs (1000, 5000, 10000) are spaced to leave room for finer priorities later; the first service in the data center to handle a request stamps a tier if none is set, and the tier then rides the entire distributed call tree. The shedder drops from the bottom of the ladder up — optional before degradable before non-degradable — and within a tier, members are hashed into buckets and whole buckets are dropped, concentrating degradation on a consistent few call trees instead of spraying it randomly across everyone; if shedding a few buckets or a lower tier isn't enough, the threshold walks up until the system stabilizes or recovers fully. Integrating this broke the original load metric: dropped requests contribute nothing meaningful to a concurrency histogram, so tier-aware shedding forced a switch to a rate-based limit — the same algorithm with throughput as the load measure — backed by per-endpoint histograms of the incoming tier and bucket distribution. That histogram is what makes the ladder honest: the shedder knows, before it drops, whether any lower-priority traffic is even present, and an endpoint receiving only degradable traffic is shed accordingly.
The last line of member protection is what happens to a dropped request. Because Hodor rejects in the filter chain, before any application logic runs, a rejection — a 503, in LinkedIn's own Rest.li service framework — is safe for the client to retry on another instance regardless of the request's idempotency. Blind retries would trade one overload for a retry storm, so budgets bound them on both sides, client and server, in a design the post credits to Google's SRE book; an exhausted server-side budget is itself a signal that the problem is widespread, at which point the server stops instructing clients to retry and accepts failed requests to protect goodput for the traffic still being served. Rollout carried the same do-no-harm posture as the design: every service ran detectors in monitoring mode over at least a week of data, including load tests, with firings correlated against performance metrics before shedding was ever enabled. The flagship Voyager services went first, replacing a hand-tuned fixed-concurrency scheme, and Hodor promptly caught overloads the manual thresholds had missed. The monitoring phase doubled as a fleet-wide profiler, surfacing GC problems, periodic application-triggered thread dumps stalling safepoints, and GC-log writes blocked on other applications' disk I/O — problems owners didn't know they had. Hodor now runs on more than 1,000 microservices and has prevented hundreds of overloads in LinkedIn production — enough success that its signals are being considered for autoscaling and safe load testing in production.
Tradeoffs
- Tiering moves the hard problem from the shedder to the taxonomy. The ladder only protects members if every request is stamped correctly at the edge and the tier survives the whole call tree — and classification is a human judgment distributed across the organization: product owners decide which features on a page are degradable, and offline jobs important enough get promoted into the degradable tier alongside member-facing traffic. A misclassified request fails silently in exactly one circumstance — overload, the only moment the label is consulted: an over-labeled batch job hides from the shedder while member traffic competes with it, and an under-labeled member flow is shed first. The spaced tier IDs are honest about this being a living system — room is reserved for finer priorities, which is also an admission that taxonomy governance never ends.
- Measuring from inside the process buys architecture-independence at the price of runtime-dependence. The rollout data showed the heartbeat signal's sensitivity shifting with the application platform — Play applications produced larger, easier-to-recognize starvation spikes than Jetty — and with the JRE itself: on Azul's Zing, which minimizes scheduling delays across threads, the background thread was often unable to detect that the service was overloaded at all. One fleet-wide threshold set had to be found that worked across all of it, and some services still weren't onboardable under the default configuration until their GC was tuned or inefficient allocation patterns were fixed. The no-tuning promise holds for the algorithm; the fleet pays for it by making outlier services conform to the detector rather than the other way around — though the post frames the forced GC fixes as significant wins in their own right.
- Precision over recall means accepting late and missed detections by design. The latency confirmation filter exists because false positives after the broad rollout — GC micro-pauses starving the heartbeat thread with no business-metric impact — were unacceptable for a system whose most important goal is never harming members. But a gate that suppresses unconfirmed firings necessarily delays action on real overloads until latency has degraded enough to confirm, and the post concedes the moving-average crossover would be inadequate as a standalone detector. The burden is placed deliberately: Hodor would rather shed late than shed wrongly, the correct asymmetry for a default-on fleet-wide system — and the opposite of what a single hand-tuned critical service might choose for itself.
- The adaptive limit deliberately remembers nothing between incidents. Recalculating from scratch is justified twice over — the traffic mix differs between overloads, and so does the mechanism — but the price is that every incident pays the rediscovery cost: an equilibrium found by feedback, then a probe cycle whose failed attempts are visible as latency spikes on the protected host in LinkedIn's own A/B graphs. Exploration is the tax on adaptivity. A system that carried capacity memory forward would recover faster when a new overload resembled the last one, and hurt members when it didn't; Hodor chose never being confidently wrong over ever being instantly right.
- The switch to rate-based shedding trades away concurrency's self-regulating property. A concurrency cap bounds work in flight, so if requests get slower, admission tightens automatically; a throughput cap admits the same request rate whether requests are cheap or expensive, leaving that correction to the detector feedback loop rather than to the metric itself. The post states only why the switch was necessary — dropped requests contribute nothing meaningful to a concurrency histogram, so tier-aware shedding could not be built on one — and does not discuss this cost; the reading here is ours. What is sourced is the compensation: per-endpoint, per-tier throughput histograms rebuilt from live traffic, restoring to the rate-based shedder the situational awareness the concurrency metric had carried implicitly.
- Retry-on-another-instance converts local drops into fleet-wide rescue only while the overload is local. The mechanism is honest about its limits: when clients signal widespread retrying, the server-side budget exhausts and the server stops instructing retries — at which point members receive failed requests, on purpose, because goodput for the traffic still being served beats a retry storm that finishes off the cluster. Dropping some requests to protect the remainder is the framework's founding bargain restated at the retry layer, and it gives Hodor's protection a floor: under a truly global overload, tiering decides who fails first, budgets decide when to stop trying to save them, and some member impact is accepted as the cost of any members being served at all.
Patterns in this article
- Priority-Aware Load Shedding
LinkedIn's instantiation is the fleet-wide default form: three tiers — optional, degradable, non-degradable — stamped at the data-center edge and carried through the distributed call tree, with member-ID hash buckets inside each tier so escalation drops whole buckets: consistent degradation for a few rather than random degradation for all. The distinctive detail is that priority broke the load metric — tier-aware shedding forced the move from a concurrency limit to a rate-based limit with per-endpoint, per-tier throughput histograms.
- Feedback-Controlled Load Management
The adaptive limit is the pattern's probe-and-backoff face: an initial cap derived from pre-overload concurrency, an equilibrium held on detector feedback, periodic upward probes, and exponential backoff when a probe re-fires the detectors — with the limit deliberately recalculated at each new overload because neither the traffic mix nor the overload mechanism repeats. No control-theory machinery; the same closed loop.
- Retry Budget
The pattern's second company, in the server-assisted form: Hodor rejects before application logic runs, so retries on another instance are safe regardless of idempotency, and budgets on both client and server — a design the post credits to Google's SRE book — bound the storm risk. The server-side budget's exhaustion doubles as a widespread-overload signal that switches retries off entirely, accepting failed requests to protect goodput.
Also solving this
Other systems in behindscale's Priority-blind load shedding class: