09 · Bounded work: queues, deadlines and ownership
Reason about backlog growth, choose an overload policy and distinguish admission, completion and cancellation without running a server.
Editorial review: · What review means
Stored in this browser only. No account, no sync. Clearing browser data removes your record.
By the end, you should be able to
- Calculate deterministic backlog growth
- Implement explicit bounded admission
- Separate timeout from cancellation and resource cleanup
Bring with you
- 08 · Retries: a timeout is not a verdict
Listen to this article
Browser / device speech · no paid TTS integration. Voice quality depends on your device.
Choose a local device voice to avoid a remote speech service. This site adds no TTS service, account or API calls.
Checking browser speech support…
Pause saves your segment; resume repeats that short segment. Changing voice or speed pauses playback. Stop resets to the beginning. Progress counts finished text segments, not audio time. Leaving or hiding this page stops or pauses speech.
What gets read aloud?
Reads the article body as it appears when you press Listen. Navigation, controls and closed sections are skipped. Expand a section, then Stop and Listen to include it. Code and equations get brief notices; figures use available labels or captions, not their visual details. This narration does not teach omitted mathematics or replace reading examples on the page.
For better sound at no added site cost, try installed English voices, including enhanced voices offered by your device. We cannot guarantee a best voice on every browser. Use Stop or your device’s audio controls if its speech engine misbehaves.
In this article · 4 sections
A queue does not create processing capacity. It trades waiting time and memory for tolerance of bursts. If a notebook importer accepts work faster than it completes work indefinitely, its backlog grows indefinitely unless it rejects, drops, coalesces or blocks some arrivals. This is a systems problem before it is a Kubernetes or cloud problem.
Choose the boundary and the units
Distinguish three counts: received requests, admitted jobs and completed jobs. A rejected request is not a completed job. A retried request may refer to a job already completed. A queue-length graph without those definitions can hide lost work or double-counted success.
For a deliberately simple deterministic model, assume a queue initially empty, 12 jobs arriving each second, and one worker completing eight jobs per second. Assume arrivals are admitted before service in each discrete time step, service is always available, and every job has the same service cost. Without a limit, backlog grows by four jobs per step. After ten steps it contains 40 jobs; no amount of optimism about an “asynchronous architecture” changes the arithmetic.
def queue_model(arrivals, service_per_tick, capacity):
if (type(service_per_tick) is not int or service_per_tick < 0
or type(capacity) is not int or capacity < 0):
raise ValueError("nonnegative integer service and capacity required")
queued = accepted = completed = rejected = 0
history = []
for count in arrivals:
if type(count) is not int or count < 0:
raise ValueError("nonnegative integer arrivals required")
admitted = min(count, capacity - queued)
accepted += admitted
rejected += count - admitted
queued += admitted
done = min(queued, service_per_tick)
queued -= done
completed += done
assert accepted == completed + queued
history.append(queued)
return {"accepted": accepted, "completed": completed,
"rejected": rejected, "queued": queued, "history": history}
unbounded_for_this_run = queue_model([12] * 10, 8, 1000)
assert unbounded_for_this_run["queued"] == 40
bounded = queue_model([12] * 10, 8, 20)
assert bounded["accepted"] == 92
assert bounded["completed"] == 80
assert bounded["rejected"] == 28
assert bounded["queued"] == 12
assert bounded["history"] == [4, 8, 12, 12, 12, 12, 12, 12, 12, 12]Why does the bounded queue end at 12 rather than 20? Capacity is checked before eight jobs finish each tick. The transient pre-service queue reaches 20, then drains to 12. Reversing arrival and service order changes the finite trace. State that order instead of presenting a made-up simulation as a measured workload.
If service lasts exactly one eighth of a second per job with one worker and no further arrivals, 40 queued jobs need five seconds to drain. Real jobs have variable costs, parallel workers, failures and scheduling overhead. A mean throughput does not establish a p99 waiting time, and a count limit is not a byte limit. Twenty jobs with 100 MB payloads are a very different memory commitment from twenty small IDs.
Bounded admission is a behavior, not just a number
Python's official queue.Queue reference documents a maxsize bound, blocking insertion, nonblocking insertion with Full, and the approximate nature of qsize()/empty()/full() under concurrency. Use the operation's result, not a separate size check, to decide whether admission succeeded.
from queue import Queue, Full, Empty
work = Queue(maxsize=2)
accepted, rejected = [], []
for name in ["ingest", "validate", "publish"]:
try:
work.put_nowait(name)
except Full:
rejected.append(name)
else:
accepted.append(name)
assert accepted == ["ingest", "validate"]
assert rejected == ["publish"]
finished = []
while True:
try:
item = work.get_nowait()
except Empty:
break
try:
finished.append(item) # stand-in for successful processing
finally:
work.task_done()
work.join()
assert finished == ["ingest", "validate"]This example is sequential. task_done() tells the queue that handling of a retrieved item is accounted for; it does not persist the result or prove the business job succeeded. In a real worker, define how failure is recorded and whether retry is rescheduled before acknowledging queue bookkeeping. join() waits for that bookkeeping to reach zero. It cannot discover whether a worker lied.
Also note a subtle API mismatch: the model above defines capacity zero as admitting nothing, while Queue(maxsize=0) means an unbounded queue. Read the concrete API, not just a variable's name. A bounded deque is different again: appending to a full bounded deque discards an item from the opposite end, which may silently lose jobs if used as an admission queue.
A process owns resources; a timeout limits waiting
A process executes a program with operating-system-managed resources. A thread is an execution stream within a process; choosing threads does not provide independent durable job state. Connections, open files and worker slots should have a named owner and a release path. The SQL lesson uses finally: con.close() because the connection must close whether assertions succeed or fail.
Do not assume that “the caller stopped waiting” means “the work stopped.” The Python subprocess documentation explicitly states that a communicate() timeout does not kill the child process; the caller must perform cleanup. By contrast, subprocess.run(..., timeout=...) kills and waits for the direct child when its timeout expires, while process creation itself may not be interruptible. This distinction is source-backed here, not demonstrated by starting long-lived processes. Cleanup of a process tree or remote job is a separate problem.
The same reasoning applies to an HTTP deadline: after a timeout, a server may continue working. Retry safety belongs to the operation's identity and effect boundary, not to the timer. A cancellation token is a request for cooperating code to stop, not a time machine that reverses already committed work.
Exercises
- After a burst, arrivals become four jobs per tick while service remains eight. How quickly does a backlog of 40 drain in the unbounded model?
- Name a workload where dropping older queued work is valid and one where it is not.
- Why can
if not work.full(): work.put_nowait(job)still raiseFullin a concurrent program? - What measurements would you need before promising a latency percentile?
Answer sketches
- Net drain is four per tick, so ten ticks, under the same arrival-before-service assumptions and without failures.
- Replacing obsolete camera preview frames may be appropriate when only the latest view matters. Silently dropping a payment or audit event is not. Even the preview policy should expose dropped-frame counts.
- Another producer may fill the queue between the check and the insertion. Catch
Fullfrom the actual insertion. - Define the end-to-end interval, collect representative arrival and service-time distributions including failures and queueing, identify the measurement population, and test under the target load. An average service rate alone is insufficient.
Exit artifact: a conservation check, a bounded admission policy, and a resource-ownership note. These models do not validate OS scheduling, thread safety of application state, real latency, cloud capacity or a distributed queue.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.