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.

Key takeaways
  • 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.
The core insight

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 PP starves if PP 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 BB such that once a process signals its intent to enter the critical section, no other process is allowed to enter more than BB 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 pp with static priority sps_p and let MM be the maximum static priority of any process in the system. If pp waits for time tt, its effective priority is sp+αts_p + \alpha t for aging rate α>0\alpha > 0. Once t≥(M−sp)/αt \geq (M - s_p)/\alpha, pp's effective priority exceeds every possible competitor's static priority (before their own aging), so pp must eventually be selected. This gives an explicit bound B=⌈(M−sp)/α⌉B = \lceil (M - s_p)/\alpha \rceil on the number of scheduling rounds pp 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 kk, exactly kk other threads (those with earlier tickets) can enter before it, and no thread that arrives later can ever draw a ticket <k< k. 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 nn) 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.

Figure
Deadlock vs. starvation vs. livelock — three distinct liveness failures
Deadlock P1 P2 circular wait — neither runs, ever Starvation P2 runs P3 runs P4 runs P1 waits (ready, never picked) system busy — P1 alone denied Livelock T1 T2 both actively retry, back off, collide, retry — CPU busy, no work done Deadlock: nobody runs. Starvation: someone never runs while others do. Livelock: everybody runs but nobody finishes. Fix comparison Deadlock → prevention/avoidance/detection (resource ordering, banker's algorithm) Starvation → aging, FIFO/ticket locks, bounded waiting Livelock → randomized exponential backoff, breaking symmetry

Complexity & correctness

Aging scheduler. Recomputing effective priority for all nn ready processes each scheduling decision costs O(n)O(n) time, and selecting the maximum costs O(n)O(n) as well (or O(log⁡n)O(\log n) with a priority-heap keyed on effective priority, at the cost of O(log⁡n)O(\log n) updates per aging step). Space overhead is O(1)O(1) per process (one wait-time counter). The correctness argument is the bound derived above: for any process pp with static priority sps_p and system-wide maximum static priority MM, waiting time is capped at

B=⌈M−spα⌉B = \left\lceil \frac{M - s_p}{\alpha} \right\rceil

scheduling rounds, after which pp's effective priority dominates every possible competitor. This is a genuine liveness proof, not a heuristic claim: it holds for any finite MM and any α>0\alpha > 0, 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 O(1)O(1) amortized per attempt (ignoring the spin itself, which is bounded by the time the kk ahead-of-you threads take in their critical sections). Space is O(1)O(1) — 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 kk waits behind exactly the threads with tickets 0,…,k−10, \dots, k-1 that

arrived earlier; since `next_ticket` only increases, no later arrival can ever acquire a ticket smaller than kk, so at most kk (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 O(1)O(1) expected work (attempt, sleep, retry). Correctness for livelock-freedom is probabilistic rather than a hard bound: if nn contending threads independently draw delays from a window that widens geometrically, the probability that thread ii's chosen window overlaps no competitor in round tt approaches 1 as tt 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 BB — 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:

TimeArrivalNaive (strict writer-priority) actionAging-corrected action
0R1 arrivesR1 starts readingR1 starts reading
1W1 arrivesR1 finishes; W1 waits (no writer yet active, R1 done) → W1 runsR1 finishes; W1 runs
2R2 arrivesW1 finishes; R2 must wait — a new writer's priority check happens before R2 is admittedR2's wait_time = 0, effective_priority = base
3W2 arrivesW2 has priority over R2 (strict writer-priority) → W2 runs, R2 still waitsR2 wait_time = 1; W2 wait_time = 0. If R2's aged priority still `<` W2's, W2 runs, but R2's counter keeps climbing
4W3 arrivesW3 runs, R2 still waits — starvation pattern emergingR2 wait_time = 2, aged priority rising
5W4 arrivesW4 runs, R2 still waitsR2'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 arrivingR2 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 B=⌈(M−sR2)/α⌉B = \lceil (M - s_{R2}) / \alpha \rceil, giving a computable, finite guarantee instead of "eventually, we hope."

A livelock trace. Two threads T1,T2T_1, T_2 each need locks AA and BB, using the polite-release protocol with `FIXED_DELAY = 5ms` for both, started 1ms apart:

Time (ms)T1T2
0acquire(A) — success—
1—acquire(A) — fails, A held by T1
1try_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)
1release(A); sleep(5)release(B); sleep(5)
6acquire(A) — successacquire(B) — success (offset preserved)
6try_acquire(B) — fails (T2 has it)try_acquire(A) — fails (T1 has it)
6release(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 t=1,6,11,16,…t=1, 6, 11, 16, \dots repeats exactly: this is livelock, not deadlock — locks are being acquired and released continuously — but neither thread ever holds both AA and BB 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

PitfallWhy it happensFix
Conflating starvation with deadlockBoth "look stuck" from the victim's point of viewCheck whether other processes are making progress; if yes, it's starvation, not deadlock
Assuming FCFS scheduling is automatically fairArrival order isn't preserved once priorities or retries reorder requestsExplicitly enforce ticket/FIFO ordering at the lock or queue level
"Polite" deadlock-avoidance causing livelockReleasing and retrying to avoid deadlock is correct for safety but can loop symmetricallyAdd randomized backoff or asymmetric tie-breaking (e.g., thread ID order)
Fixed (non-random) retry delayDeterministic timing keeps colliding threads in lockstepUse randomized, exponentially growing backoff windows
Aging with too-small a rate α\alphaBound B=⌈(M−sp)/α⌉B = \lceil (M-s_p)/\alpha \rceil becomes astronomically large, "starvation-free" only in theoryTune α\alpha so worst-case bound is acceptable for the workload
Ignoring cache/hardware unfairness in spinlocksTest-and-set locks can be practically unfair even though the algorithm looks symmetric on paperUse ticket locks or MCS/CLH queue locks for hardware-level fairness
Testing concurrency fixes only under light loadStarvation and livelock are load-dependent; light load hides bothStress-test under sustained high contention and adversarial arrival patterns

Pioneers & history

Origins · 1 minEdsger W. Dijkstra and how this was discovered
EW
Edsger W. Dijkstra
1930-2002 · Formalized mutual exclusion, semaphores, and the dining philosophers problem, exposing starvation as a distinct hazard from deadlock

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.

History
1965
— Dijkstra formalizes mutual exclusion and introduces semaphores, framing the correctness properties later split into safety, progress, and bounded waiting.
1971
— Dijkstra's dining philosophers problem becomes the canonical example separating deadlock (all philosophers stuck) from starvation (one philosopher perpetually outpaced).
1974
— Lamport's bakery algorithm gives a FIFO-ticket-style mutual exclusion protocol with an explicit bounded-waiting guarantee, without special atomic hardware instructions.
1989
— Mellor-Crummey and Scott (MCS locks) and Anderson (queue locks) generalize ticket-style fairness to scalable, cache-friendly queue locks for multiprocessors.
1989
— Ethernet's exponential backoff (from the earlier ALOHA and CSMA/CD work of the 1970s–80s) becomes the textbook example of randomized backoff curing livelock-like repeated collisions in shared media.

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 α>0\alpha > 0 and a bounded maximum priority MM in the system, aging provides a hard, computable bound B=⌈(M−sp)/α⌉B = \lceil (M - s_p)/\alpha \rceil on how many scheduling rounds any process can be passed over. That is a genuine, provable elimination of starvation, not just a mitigation — after BB rounds the aged process's effective priority strictly dominates every possible competitor's static priority. The practical caveat is that BB can be large if α\alpha is small or MM 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.

Before this