Starvation and Livelock
Imagine a busy print server that always serves the shortest job first. Small jobs keep arriving. One enormous report waits forever behind a stream of one-page memos. Nothing is deadlocked — the printer is constantly working. Yet that one job never runs. This is starvation, indefinite postponement of a runnable process even though the system as a whole keeps making progress. A subtler cousin, livelock, occurs when processes are not blocked at all. Instead, they keep changing state in response to each other. None of them completes its work. It's like two people repeatedly stepping aside for each other in a narrow hallway, never getting through. Both phenomena matter because they violate a promise that concurrent systems implicitly make. That promise is that every request submitted will eventually be honored. Deadlock: Definition and Conditions teaches you to recognize when processes are permanently stuck waiting on each other in a cycle. Starvation and livelock are different failure modes entirely. The system is live (things are happening), but fairness has broken down. Real schedulers, lock implementations, and network protocols must be engineered explicitly against these failures. This is because the absence of deadlock says nothing about the absence of starvation. The big picture is that concurrency correctness has at least three independent axes. These are safety (nothing bad ever happens, e.g., no two threads in a critical section simultaneously), deadlock-freedom (the system never gets stuck with no runnable process able to proceed), and fairness/liveness (every waiting process eventually gets served). A system can satisfy the first two and still fail the third catastrophically. Priority schedulers, naive lock-free retry loops, and even TCP congestion control have all shipped with real starvation or livelock bugs. The fixes — aging, fair queuing, randomized backoff, ticket locks — form a recurring toolkit. This toolkit spans operating systems, databases, and distributed systems. This article gives you precise definitions that distinguish starvation and livelock from deadlock. It offers a taxonomy of where they arise: scheduling, locking, the The Readers-Writers Problem family, and busy-wait protocols. It provides worked pseudocode for both a starvation-prone system and its fairness-corrected version. It also gives a formal argument for why aging and bounded-waiting locks guarantee progress. Finally, it tours the historical fixes that solved these problems in production systems. These range from Dijkstra's semaphore refinements to ticket locks and exponential backoff.
- Starvation is indefinite postponement of a ready process despite the system making global progress; deadlock is no process making progress.
- Livelock is starvation's active twin: processes keep changing state (retrying, yielding, backing off) but no one finishes.
- Common causes: strict priority scheduling, unfair lock implementations, greedy resource-ordering heuristics, and symmetric retry logic.
- Standard cures: aging (raise priority with wait time), FIFO/ticket/bounded-waiting locks, randomized exponential backoff, and fair queuing.
- Bounded waiting is a formal liveness property: it caps the number of times other processes can overtake a waiter, which mathematically forbids starvation.
- Fixing starvation/livelock often costs throughput or adds overhead — fairness is a design tradeoff, not a free lunch.
A system free of deadlock can still fail every user, one at a time, forever — starvation and livelock are separate liveness failures that require their own explicit remedies (aging, bounded waiting, and randomized backoff), not just deadlock avoidance.
The idea
Starvation (also called indefinite postponement or lockout) occurs when a process that is ready to run, and requesting resources it is entitled to, is repeatedly denied service while other processes continue to be served. Formally, in a system that runs forever, a process starves if is enabled (able to make progress) infinitely often but executes its critical action only finitely many times, or never. The defining feature is that the system overall is live — global throughput is nonzero, other processes complete — but one participant is singled out and denied forever, or for an unbounded time.
Livelock is a special, more dramatic case of starvation: the affected processes are not idle or blocked, they are actively executing, but their execution consists only of reacting to each other without any of them completing useful work. A classic mental image is two people in a corridor, both stepping left, then both stepping right, in perfect mirrored deference, forever. In systems terms, livelock typically arises from symmetric conflict-avoidance protocols: each thread detects contention, backs off, and retries, but the retry decisions correlate (e.g., all threads use the same fixed delay) so the same collision repeats indefinitely. Unlike ordinary starvation, where the victim is merely unlucky, livelock can afflict all participants simultaneously — none of them make progress even though CPU cycles are being spent every instant. Both are cleanly distinguished from deadlock. Deadlock requires a set of processes each waiting for a resource held by another in the set, forming a cycle in the resource-allocation graph, so that no process in the cycle can ever proceed — the system is not just unfair, it is frozen for those processes. Starvation requires no such cycle; resources are being granted and released constantly, just never (or too rarely) to the starving process. The standard liveness property that rules starvation out by construction is bounded waiting: a mutual-exclusion protocol satisfies bounded waiting if there exists a constant such that once a process signals its intent to enter the critical section, no other process is allowed to enter more than times before it does. Any implementation that provably enforces bounded waiting is starvation-free by definition — this is why textbook proofs of correctness for entry protocols always include a bounded-waiting argument alongside mutual exclusion and progress.
How it works
The mechanics of starvation and livelock are best understood by first showing a natural but flawed policy, then the standard repair.
Where starvation creeps in — priority scheduling. A pure static-priority scheduler always dispatches the highest-priority ready process. If high-priority processes keep arriving, a low-priority process can wait forever even though the CPU is constantly busy running someone. This is exactly the "starving low-priority job" pattern that shows up in the readers–writers problem when writer-priority readers-writers policies let a continuous stream of writers block all readers indefinitely (or vice versa for reader-priority policies blocking writers) — a direct extension of The Readers-Writers Problem.
The remedy — aging. Aging tracks how long a process has waited and gradually increases its effective priority, guaranteeing that no process can wait longer than some bound before it becomes the highest-priority ready process and gets scheduled. ```pseudocode // Starvation-prone scheduler: pure static priority function SCHEDULE_NAIVE(ready_queue): loop forever: p = process in ready_queue with max(static_priority) // tie-break arbitrarily, e.g., first found dispatch(p) // p runs until it blocks or is preempted by a strictly higher-priority arrival // Problem: if arrivals of higher-priority processes never stop, // a lower-priority process can remain in ready_queue forever. // Starvation-free scheduler: aging added function SCHEDULE_WITH_AGING(ready_queue, AGE_RATE): loop forever: for each p in ready_queue: p.effective_priority = p.static_priority + AGE_RATE p.wait_time p = process in ready_queue with max(effective_priority) dispatch(p) p.wait_time = 0 // reset once served for each q in ready_queue, q != p: q.wait_time += quantum_length // everyone else keeps aging ``` Why aging bounds waiting:* fix a process with static priority and let be the maximum static priority of any process in the system. If waits for time , its effective priority is for aging rate . Once , 's effective priority exceeds every possible competitor's static priority (before their own aging), so must eventually be selected. This gives an explicit bound on the number of scheduling rounds can be passed over — a bounded-waiting guarantee constructed by design.
Where starvation creeps in — lock implementations. A spinlock built from a single shared flag using test-and-set gives no ordering guarantee: whichever thread's CPU next notices the flag is free wins the race, and under contention this can (in principle, and sometimes in practice on real hardware with unfair cache/interconnect behavior) let one thread win the race indefinitely while another thread is repeatedly denied. ```pseudocode // Unfair lock: no bound on how long a waiter can be overtaken function LOCK_NAIVE(flag): while TestAndSet(flag) == true: pass // busy-wait; any waiter can "win" the retry race function UNLOCK_NAIVE(flag): flag = false ```
The remedy — ticket lock (bounded waiting by construction). A ticket lock assigns each arriving thread a strictly increasing ticket number, using an atomic fetch-and-increment, and serves threads in ticket order — this is FIFO fairness enforced structurally, not probabilistically. ```pseudocode // Ticket lock: FIFO order guarantees bounded waiting shared now_serving = 0 shared next_ticket = 0 function LOCK_TICKET(): my_ticket = FetchAndIncrement(next_ticket) // atomic while now_serving != my_ticket: pass // spin, but order is fixed // critical section function UNLOCK_TICKET(): now_serving = now_serving + 1 ``` Why this bounds waiting: the moment a thread draws ticket , exactly other threads (those with earlier tickets) can enter before it, and no thread that arrives later can ever draw a ticket . So the number of processes that can overtake a waiter is bounded by the number already ahead of it at arrival time — a hard, countable bound, satisfying bounded waiting exactly.
Where livelock creeps in — symmetric backoff. Suppose two threads each try to acquire two locks in the same order but back off identically on conflict: ```pseudocode // Livelock-prone: deterministic, synchronized retry function POLITE_ACQUIRE(my_lock, other_lock): loop forever: acquire(my_lock) if try_acquire(other_lock): return // success, proceed else: release(my_lock) // be "polite", avoid deadlock sleep(FIXED_DELAY) // BUG: same delay for everyone // retry from top ``` If two threads run this symmetrically with the same `FIXED_DELAY` and near-simultaneous start times, they can release-and-retry in lockstep forever: each grabs `my_lock`, finds `other_lock` taken, releases, sleeps the same fixed interval, and collides again. No deadlock (locks are always eventually released), but no progress either — this is livelock.
The remedy — randomized exponential backoff. Breaking the symmetry with randomness (and increasing the delay range on repeated failure) makes simultaneous re-collision increasingly improbable. ```pseudocode function ROBUST_ACQUIRE(my_lock, other_lock): attempt = 0 loop forever: acquire(my_lock) if try_acquire(other_lock): return release(my_lock) attempt = attempt + 1 delay = random(0, min(MAX_DELAY, BASE * 2^attempt)) // randomized, growing sleep(delay) ``` Randomization destroys the deterministic symmetry that caused livelock: the probability that two (or ) threads choose colliding retry windows shrinks geometrically with each retry round, so with probability 1 some thread eventually wins a collision-free window and completes.
Complexity & correctness
Aging scheduler. Recomputing effective priority for all ready processes each scheduling decision costs time, and selecting the maximum costs as well (or with a priority-heap keyed on effective priority, at the cost of updates per aging step). Space overhead is per process (one wait-time counter). The correctness argument is the bound derived above: for any process with static priority and system-wide maximum static priority , waiting time is capped at
scheduling rounds, after which 's effective priority dominates every possible competitor. This is a genuine liveness proof, not a heuristic claim: it holds for any finite and any , independent of the arrival pattern of other processes, because effective priority increases monotonically and unboundedly with wait time while true competitors' static components are fixed.
Ticket lock. `FetchAndIncrement` and the spin check are each amortized per attempt (ignoring the spin itself, which is bounded by the time the ahead-of-you threads take in their critical sections). Space is — two shared counters, regardless of the number of contending threads. Correctness has three parts, mirroring the classical three-part proof structure for mutual exclusion:
- Mutual exclusion: only the thread with `my_ticket == now_serving` proceeds, and `now_serving` increases by
exactly one per release, so at most one ticket value is "being served" at any time.
- Progress: `now_serving` always advances (assuming critical sections terminate), so eventually every ticket
value is reached — no process can be skipped and no ticket value is ever served twice.
- Bounded waiting: a thread with ticket waits behind exactly the threads with tickets that
arrived earlier; since `next_ticket` only increases, no later arrival can ever acquire a ticket smaller than , so at most (a fixed, finite number determined at arrival time) threads can enter before it. This is bounded waiting by construction, hence starvation-free. The tradeoff: ticket locks impose strict FIFO order even when a different order might have higher throughput (e.g., a thread that just released the lock and still has the relevant cache lines hot might be a better next holder), so fairness here is bought at some potential cache-locality cost compared to unfair test-and-set locks.
Randomized backoff. Each retry round costs expected work (attempt, sleep, retry). Correctness for livelock-freedom is probabilistic rather than a hard bound: if contending threads independently draw delays from a window that widens geometrically, the probability that thread 's chosen window overlaps no competitor in round approaches 1 as grows (the window size, and hence the chance of a clean slot, grows exponentially while the number of competitors that remain unresolved shrinks in expectation). Formally, this is an argument of progress "with probability 1" (almost-sure termination) rather than a deterministic bound like the ticket lock's — an important honesty point: randomized backoff eliminates livelock almost surely, not by a hard worst-case guarantee, and adversarial scheduling can in principle still delay termination, though not indefinitely with probability 1.
Honest tradeoff summary. Deterministic fairness mechanisms (aging, ticket locks) give hard, provable bounds but add bookkeeping overhead and can reduce peak throughput or cache locality. Randomized mechanisms (backoff) are cheap and scale well under heavy contention but only guarantee progress probabilistically, and pathological adversarial timing (rare in practice, but not impossible in theory) could delay a particular thread arbitrarily long, though the expected wait remains bounded.
Worked example
Trace a concrete readers–writers scenario with a naive writer-priority policy, then the aging fix, to see starvation appear and disappear.
Setup. Reader-priority variant: a writer may enter only when no reader is currently reading and none is waiting with higher priority; suppose writers are given strict priority so that any waiting writer is served before any newly arriving reader. Requests arrive in this order (time increases downward), each writer taking one time unit and finishing quickly, each reader wanting to read for one time unit:
| Time | Arrival | Naive (strict writer-priority) action | Aging-corrected action |
|---|---|---|---|
| 0 | R1 arrives | R1 starts reading | R1 starts reading |
| 1 | W1 arrives | R1 finishes; W1 waits (no writer yet active, R1 done) → W1 runs | R1 finishes; W1 runs |
| 2 | R2 arrives | W1 finishes; R2 must wait — a new writer's priority check happens before R2 is admitted | R2's wait_time = 0, effective_priority = base |
| 3 | W2 arrives | W2 has priority over R2 (strict writer-priority) → W2 runs, R2 still waits | R2 wait_time = 1; W2 wait_time = 0. If R2's aged priority still `<` W2's, W2 runs, but R2's counter keeps climbing |
| 4 | W3 arrives | W3 runs, R2 still waits — starvation pattern emerging | R2 wait_time = 2, aged priority rising |
| 5 | W4 arrives | W4 runs, R2 still waits | R2's aged priority now exceeds any incoming writer's base priority → R2 is scheduled next, before W4 |
| 6 | — | R2 waits indefinitely as long as writers keep arriving | R2 finishes; writers resume afterward |
In the naive column, as long as writers arrive faster than R2 can be squeezed in, R2 is a textbook starving reader: ready, never denied by any cycle (so not deadlock), yet never scheduled — an unbounded number of writers can overtake it. In the aging column, R2's `wait_time` grows every round it is passed over, so its `effective_priority` eventually exceeds any writer's static priority, forcing the scheduler to admit R2 no later than round , giving a computable, finite guarantee instead of "eventually, we hope."
A livelock trace. Two threads each need locks and , using the polite-release protocol with `FIXED_DELAY = 5ms` for both, started 1ms apart:
| Time (ms) | T1 | T2 |
|---|---|---|
| 0 | acquire(A) — success | — |
| 1 | — | acquire(A) — fails, A held by T1 |
| 1 | try_acquire(B) — fails, T2 doesn't hold B yet, but suppose T2 grabbed B at t=0.5 | (T2 got B at 0.5, tries A, fails) |
| 1 | release(A); sleep(5) | release(B); sleep(5) |
| 6 | acquire(A) — success | acquire(B) — success (offset preserved) |
| 6 | try_acquire(B) — fails (T2 has it) | try_acquire(A) — fails (T1 has it) |
| 6 | release(A); sleep(5) | release(B); sleep(5) |
| 11 | ... repeats identically ... | ... repeats identically ... |
Because both threads use the identical fixed delay and their relative offset never changes, the pattern at repeats exactly: this is livelock, not deadlock — locks are being acquired and released continuously — but neither thread ever holds both and simultaneously. Switching `sleep(5)` to `sleep(random(0, 2^attempt * BASE))` breaks the fixed 5ms offset: after a few rounds the random delays desynchronize enough that one thread's retry lands in a window where the other is not contending, and it acquires both locks.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Conflating starvation with deadlock | Both "look stuck" from the victim's point of view | Check whether other processes are making progress; if yes, it's starvation, not deadlock |
| Assuming FCFS scheduling is automatically fair | Arrival order isn't preserved once priorities or retries reorder requests | Explicitly enforce ticket/FIFO ordering at the lock or queue level |
| "Polite" deadlock-avoidance causing livelock | Releasing and retrying to avoid deadlock is correct for safety but can loop symmetrically | Add randomized backoff or asymmetric tie-breaking (e.g., thread ID order) |
| Fixed (non-random) retry delay | Deterministic timing keeps colliding threads in lockstep | Use randomized, exponentially growing backoff windows |
| Aging with too-small a rate | Bound becomes astronomically large, "starvation-free" only in theory | Tune so worst-case bound is acceptable for the workload |
| Ignoring cache/hardware unfairness in spinlocks | Test-and-set locks can be practically unfair even though the algorithm looks symmetric on paper | Use ticket locks or MCS/CLH queue locks for hardware-level fairness |
| Testing concurrency fixes only under light load | Starvation and livelock are load-dependent; light load hides both | Stress-test under sustained high contention and adversarial arrival patterns |
Pioneers & history
Origins · 1 minEdsger W. Dijkstra and how this was discovered
Dijkstra's 1965 solution to mutual exclusion and his later dining philosophers problem (1971) made clear that avoiding deadlock is not the same as guaranteeing every process is served. His semaphore-based constructions, and subsequent refinements by others to add bounded waiting, established the vocabulary — mutual exclusion, progress, bounded waiting — still used to state correctness for any lock today.
Where this matters
Operating system schedulers (Linux's Completely Fair Scheduler, Windows' priority boosting for starved threads) build in explicit anti-starvation mechanisms because pure priority scheduling is known to starve low-priority work. Database engines use aging or wound-wait/wait-die schemes not just to avoid deadlock but to prevent transactions from being perpetually aborted and restarted by younger, more aggressive transactions. Network protocols — Ethernet's CSMA/CD, TCP's congestion-control backoff, wireless MAC layers — rely on randomized exponential backoff precisely to prevent livelock-like collision loops between competing senders. Concurrent data structure libraries (Java's `java.util.concurrent`, Linux kernel queued spinlocks) implement ticket and MCS locks specifically because naive spinlocks can be starvation-prone under real cache-coherence traffic patterns, even though they are deadlock-free on paper. Distributed consensus and leader-election protocols must guard against livelock when multiple nodes simultaneously detect a leaderless state and race to become leader, a scenario directly analogous to the two-thread polite-lock livelock traced above, usually resolved with randomized election timeouts (as in Raft).
Ask the tutor: How would you redesign a writer-priority readers-writers lock to guarantee bounded waiting for readers without sacrificing writer throughput too much?
Is starvation just a slow-motion deadlock?
No — they are structurally different failures. Deadlock requires a cycle of processes each waiting on a resource held by the next, so that none of them can ever proceed; the resource-allocation graph is genuinely stuck. Starvation requires no cycle at all: resources are being granted and released continuously, and other processes keep completing work, but one specific process is repeatedly passed over. You can detect deadlock by finding a cycle in a wait-for graph; you cannot detect starvation that way, because there is no cycle to find. Starvation is fundamentally a scheduling/fairness property over the long run, not a snapshot-graph property, which is why it needs its own remedies (aging, bounded waiting) rather than deadlock-style cycle detection.
Can a system be deadlock-free but still starve processes?
Yes, and this is one of the most important points in the topic. Deadlock-freedom only guarantees that some process can always make progress; it says nothing about which process. A strict priority scheduler is typically deadlock-free — the CPU is never permanently idle while ready processes exist — yet it can starve low-priority processes forever if higher-priority work keeps arriving. This is exactly why operating systems courses treat deadlock-avoidance (banker's algorithm, resource ordering) and starvation-avoidance (aging, fair queuing) as separate mechanisms solving separate problems, even though both fall under "concurrency correctness."
How is livelock different from busy-waiting in an ordinary spinlock?
Ordinary busy-waiting in a spinlock is productive waiting: the spinning thread will eventually see the lock released and proceed, and the total time spent spinning is bounded by however long the lock's current holder needs. Livelock is different because there is no eventual success even in principle under the given dynamics — the threads' actions actively cause each other to keep retrying, with no guarantee (deterministic or probabilistic, until backoff is randomized) that the pattern ever breaks. The key distinguishing question is: if you let the system run forever unmodified, does any participant eventually finish? In plain spinlocks, yes. In symmetric livelock, not necessarily.
Does aging ever fully solve starvation, or just make it "less bad"?
When implemented correctly with a fixed aging rate and a bounded maximum priority in the system, aging provides a hard, computable bound on how many scheduling rounds any process can be passed over. That is a genuine, provable elimination of starvation, not just a mitigation — after rounds the aged process's effective priority strictly dominates every possible competitor's static priority. The practical caveat is that can be large if is small or is large, so aging solves starvation in the formal sense while still allowing uncomfortably long, though finite, worst-case waits in poorly tuned systems.
Why doesn't simply randomizing which process runs next also fix starvation?
Pure random scheduling (pick a uniformly random ready process each round) does reduce the expected wait for any given process, and in fact gives starvation-freedom with probability 1 by a Borel–Cantelli-style argument, since the probability of never being chosen across infinitely many independent trials is zero. However, it gives no deterministic worst-case bound: an adversarial run of random draws could, with vanishingly small but nonzero probability, skip one process for an arbitrarily long (though almost-surely finite) time. This is the same honesty distinction as randomized backoff: probabilistic fixes give almost-sure progress, while structural fixes like aging or ticket locks give hard, computable bounds — the two are not equivalent guarantees.
Is livelock only a two-thread phenomenon, like the corridor example?
No, livelock generalizes to any number of participants and appears in distributed systems constantly. A classic case is multiple nodes simultaneously detecting a leaderless cluster and all initiating an election at once; if their election timeouts are synchronized, they can repeatedly split votes and restart elections without any node ever winning — a distributed livelock. Raft's fix is randomized election timeouts, structurally identical to the randomized-backoff fix for the two-thread lock example: breaking synchronized, deterministic retry timing with randomness so that, with high probability, exactly one participant's retry window is uncontested.
Where does the readers-writers problem intersect with starvation specifically?
The classical readers-writers problem has multiple scheduling policies (reader-priority, writer-priority, fair), and the "naive" versions of the first two are textbook starvation examples: reader-priority can starve writers indefinitely if readers never stop arriving, and writer-priority can starve readers indefinitely if writers never stop arriving. This is precisely why the fair (third) readers-writers policy exists: it typically uses a queue or turnstile so that once a writer (or reader) is waiting, no new arrival of the opposite type can jump ahead of it, giving a bounded-waiting guarantee analogous to the ticket lock's. It is a direct, worked application of the aging/FIFO ideas in this article to the specific resource-sharing pattern of readers and writers.