skip to content

You ask a scheduler to run a piece of work once, 100 milliseconds from now. Explain what that request actually guarantees, how a scheduler typically decides what to run next, and why the work might start noticeably later than 100 ms.

level: juniorimportance: must knowfreq 50%

answer

  1. delay = lower bound, never an appointment
  2. due time = now + delay; priority queue by due time
  3. timer thread sleeps until head.due, then dispatches
  4. late because: workers busy, timer granularity, OS latency, GC/throttle pause
  5. measure start latency = start − due; monotonic clock for delays

basics

~20 s

A delay is a lower bound, not an appointment: the task will not start before 100 ms, but it may start much later. The scheduler keeps pending tasks ordered by due time and can only run one when a worker is free and the clock has passed it.

solid answer

~50 s

A delayed task carries a **due time** = now + delay. The scheduler keeps pending tasks in a priority order by due time (conceptually a min-heap keyed on deadline), sleeps until the earliest due time, then hands ready tasks to workers. The guarantee is one-sided: **not before** the due time. Actual start can be later for several reasons — all its workers are busy running earlier tasks or long-running ones; the operating system did not wake the scheduler exactly on time (timer granularity and OS scheduling latency, typically milliseconds); the process was paused by a garbage-collection or page-fault stall; or the machine is heavily loaded. This is why a scheduled task's real property is *start latency*: due time to actual start. Anything that must happen at an exact instant needs a different design — you cannot get "exactly at T" from a general-purpose scheduler, only "at or after T, usually soon after."

code

text · 15 lines
text
schedule(task, delay):
  task.due = monotonicNow() + delay
  heap.insert(task)                 # ordered by due time
  wakeTimerThread()                 # in case this is the new earliest

timerThread loop:
  wait(until = heap.peek().due)     # approximate: OS timer granularity
  while heap.peek().due <= monotonicNow():
    workers.submit(heap.pop())      # waits here if all workers are busy

timeline (1 worker):
t=0     schedule(T, delay=100ms)   -> due=100
t=0     worker starts a 2s task
t=100   T is due                    -> eligible, but no worker free
t=2000  worker frees                -> T starts, 1900ms of start latency

go deeper

for a junior

State the guarantee in one line — not before the delay elapses, possibly later — and name the two obvious causes: all workers busy, and the OS not waking the scheduler exactly on time.

for a middle

Describe the mechanism: a priority queue ordered by due time, a timer thread that sleeps until the head is due, then dispatch to workers. Mention timer granularity and start latency as a metric.

for a senior

Add the operational causes of jitter — runtime pauses, container CPU throttling, saturated scheduler workers — plus monotonic versus wall-clock measurement, and the practice of delegating long work off the scheduler's own workers.

for a principal

Frame it as a timing contract: what the platform can and cannot promise, when to move to a real-time or dedicated mechanism, and how scheduling jitter interacts with SLAs and with downstream systems that assume punctuality.

## What you are actually asking for "Run this in 100 ms" is shorthand for "do not run this before now + 100 ms." The scheduler makes a one-sided promise. It never promises an upper bound, and no general-purpose scheduler can, because it does not control the operating system, other tasks, or the machine. Writing it out: due = now + delay. The task becomes *eligible* at `due`. It *starts* at some time ≥ due. The gap between them is **start latency** (also called scheduling delay or jitter), and it is the number worth thinking about. ## How a scheduler is usually built Almost every delayed-task scheduler has the same shape: 1. A **priority queue ordered by due time** holds pending tasks. The head is the task due soonest. Insert and remove are logarithmic in the number of pending tasks, so holding many timers is cheap. 2. A **timer thread** looks at the head, computes `head.due − now`, and sleeps for that duration (or waits on a condition with that timeout, so a newly scheduled earlier task can wake it up immediately). 3. On waking, it removes every task whose due time has passed and **hands them to workers** for execution. A critical consequence of step 3: on a scheduler with a single worker, the timer thread and the executing task may be the same thread, so a long-running task delays everything behind it. Even with multiple workers, if all of them are busy, a task that became due simply waits — it is now an ordinary queued task. ## Why the start is late **All workers busy.** The most common cause and the most controllable. If the scheduler has one worker and a previous task runs for two seconds, a task due 100 ms from now starts nearly two seconds late. This is why mixing long-running work into a scheduler with a small worker count produces mysterious timing. **Timer granularity.** A sleep or wait with a timeout is itself approximate. Operating systems wake threads on a timer tick or via a timer subsystem with finite resolution; a request for 100 ms commonly returns after 101–115 ms depending on the platform and power state. Some platforms coalesce timers to save energy, deliberately batching wakeups. **OS scheduling latency.** Being *runnable* is not being *running*. After the timer fires, the thread must be picked by the OS scheduler. On a loaded machine, or one where other processes compete for CPU, that adds anywhere from microseconds to tens of milliseconds. **Runtime pauses.** A garbage-collection pause, a page fault, swapping, or a container being CPU-throttled after exhausting its quota all freeze the process regardless of what your scheduler wanted. Throttled containers are a frequent and surprising cause of hundreds of milliseconds of jitter. **Clock choice.** A well-built scheduler measures delays on a **monotonic** clock — one that only moves forward at a steady rate — rather than on wall-clock time, which can jump when the machine synchronizes time or a human changes it. If a scheduler uses wall-clock time for relative delays, a backward time correction can make a task appear not yet due and delay it by the size of the correction. ## What this means in practice - Treat the delay as a floor. If your logic requires "not later than T," you need a timeout or a deadline check inside the task, not just a schedule. - Do not chain assumptions: "I scheduled A at +100 ms and B at +200 ms, therefore A finishes before B starts" is false. Ordering of *start* follows due time; ordering of *completion* follows nothing unless you enforce it. - Measure start latency, not just "did it run." Record the due time in the task and log or emit the difference at start. It is the single most useful metric for a scheduler, and it degrades quietly. - Keep the scheduler's workers free. If the scheduled work is long, have the scheduled task submit to a *separate* pool and return immediately, so the timing thread is never the bottleneck. - If you truly need sub-millisecond precision, a general-purpose scheduler is the wrong tool; that requires busy-waiting, real-time scheduling priorities, or a dedicated real-time system. ## A note on cancellation A delayed task that has not yet started can usually be cancelled cleanly — it is just removed from the priority queue. Once it has started, cancellation depends on the task cooperating (checking a flag or a cancellation signal). This distinction matters because "cancel" is often assumed to be reliable when it is only reliable before the due time.

  • How would you detect that scheduled tasks are consistently starting late?
    Stamp each task with its due time when it is scheduled, and at the moment execution begins record the difference between the actual start and the due time. Emit that as a start-latency metric with percentiles, per task type. A rising p99 usually means the scheduler's workers are saturated by long-running tasks, or the process is being paused or CPU-throttled.
  • Why should a scheduler use a monotonic clock rather than wall-clock time to measure a relative delay?
    Wall-clock time can jump forward or backward when the machine synchronizes with a time source or an operator changes it, so a relative delay measured against it can fire far too early or be postponed by the size of the correction. A monotonic clock only advances forward at a steady rate and is unaffected by such adjustments, which is exactly what "100 ms from now" means. Wall-clock time is still the right basis for absolute schedules like "at 03:00".
  • You need work to start within 10 ms of its due time. Can a general-purpose scheduler give you that?
    Not as a guarantee. You can improve the odds — dedicate workers so none are busy, keep scheduled tasks short by delegating heavy work to another pool, avoid runtime pauses and container CPU throttling — but timer granularity and OS scheduling latency remain. Hard bounds require real-time scheduling support, busy-waiting, or a dedicated real-time environment.

An oven timer, not a train timetable. The timer tells you the earliest moment the dish may come out; if you are on the phone when it dings, it comes out later. Nothing about the timer promises you will be free at that instant.

saying these in an interview costs you the question

  • Believing the delay is an exact appointment rather than an earliest-start time.
  • Assuming a task scheduled earlier always completes before one scheduled later.
  • Running long work directly on a scheduler with few workers, then blaming the scheduler for drift.
  • Using wall-clock time to measure relative delays, so time synchronization changes the firing.
  • Thinking cancellation is always effective — it reliably works only before the task starts.

context