Hermes Wiki

Priority Scheduling and Starvation: Aging as the Fix

Concept

Priority scheduling assigns every item in a queue a priority value and always dispatches the highest-priority item next, regardless of how long lower-priority items have been waiting. This is a deliberate rejection of FIFO's fairness guarantee: arrival order stops mattering the moment something more important shows up. Operating-system CPU schedulers, message-queue systems (a "high" queue drained before a "low" queue), and real-time systems all use some form of this — the OS case is the clean canonical example, because it exposes the failure mode most starkly.

A scheduler can implement priority scheduling as preemptive (a newly-arrived higher-priority item interrupts whatever is currently running) or non-preemptive (the currently-running item finishes its turn even if something higher-priority arrives mid-execution). Preemptive gets a higher-priority item running sooner, at the cost of an extra context switch and the interrupted item's partial progress being shelved. Non-preemptive is simpler and avoids that overhead, but a long-running low-priority item can block a newly-arrived urgent one for its entire remaining runtime.

The problem both variants share is starvation: if a steady stream of higher-priority work keeps arriving, a low-priority item can wait indefinitely — not just a long time, but literally forever, since nothing in the base algorithm ever favors it. The standard fix is aging — gradually raising a waiting item's effective priority the longer it sits in the queue, until it eventually out-ranks the newer high-priority arrivals and gets serviced. A commonly cited concrete scheme: priorities numbered 0 (highest) to 127 (lowest), incrementing a waiting item's priority by 1 every fixed interval (e.g. 15 minutes) — under that scheme even an item that started at the lowest priority is guaranteed to age up to the highest tier within a bounded time, capping worst-case wait no matter how much higher-priority work keeps arriving.

Tradeoffs

Variant Responsiveness for high-priority work Starvation risk for low-priority work Overhead
Non-preemptive priority Waits for current item to finish, even if lower-priority Same starvation risk as preemptive — priority order alone doesn't fix it Lowest — no extra context switches
Preemptive priority Highest — interrupts immediately Same risk unless aging is added Higher — context switch + shelved partial work per preemption
Priority + aging (either variant) Slightly less immediate (an aged-up low item competes for the slot) Bounded — every item is guaranteed to eventually age into the top tier Small extra bookkeeping — a periodic priority-recompute pass
Plain FIFO (no priority at all) None — urgent work waits its turn like everything else None — strict arrival order Lowest, no priority bookkeeping at all

Pure priority scheduling without aging is rarely the right end state for anything user-facing — it's a starvation bug waiting for a workload pattern (sustained high-priority arrival rate) to trigger it in production, not a deliberate tradeoff.

When to use / when not to

  • Use priority scheduling wherever some items genuinely can't wait behind others regardless of arrival order — an incident/critical alert queue over routine notifications, a real-time trading system's cancel-order messages over new-order messages, an OS scheduler favoring interactive/foreground processes over batch background jobs.
  • Always pair it with aging (or an equivalent bound, like a small fixed number of priority tiers plus a hard maximum wait) unless every item genuinely has to be draining a bounded, finite backlog — an unbounded, continuously-refilled queue with no aging is a starvation bug by construction.
  • Prefer a small number of discrete priority tiers (e.g. 3-5 levels) over a large continuous priority range — a continuous range makes the aging math harder to reason about and rarely reflects a real distinction in urgency between adjacent values.
  • Don't reach for priority scheduling as a default — FIFO is simpler, is starvation-free by construction, and is the right choice unless a specific requirement genuinely demands letting some items jump the queue.

Common pitfall

Adding priority tiers to a queue without an aging mechanism (or any other starvation bound) because the failure mode doesn't show up in testing — a light load with occasional high-priority items looks fine, since the queue drains fast enough that low-priority items never wait long regardless. The starvation bug only becomes visible under sustained load, when high-priority arrivals never let up long enough for the queue to drain — exactly the condition a production system hits during an incident or traffic spike, which is the worst possible time to discover that low-priority work (which is often still user-facing — a routine request, not truly disposable) has been silently starved for hours.

Engineering Lens

The design-review question isn't "does this queue have priorities" — it's "what is the worst-case wait time for the lowest-priority item under sustained high-priority load, and is that bound enforced by the algorithm or just assumed from typical traffic patterns." Aging turns "priority scheduling" from a fairness gamble into a system with a provable worst-case bound, which is the difference between a design that survives an incident and one that gets discovered as broken during one.

Sources

Hermes Wiki