advanced26 min studyLesson 166 in Engineering

Completion queues and buffer lifetimes

Track request identities, completion entries, and application ownership to decide when asynchronous I/O buffers can safely return to a pool.

On this page

What you’ll understand

  • Separate completion-ring slots from the payload buffers they describe.
  • Apply ownership accounting across submission, terminal completion, and consumption.
  • Size a buffer pool from explicit lifetime assumptions rather than submission depth alone.

An asynchronous read has at least three objects with different lifetimes: a submission descriptor, a completion record, and the memory holding returned bytes. Reusing one does not automatically permit reuse of the others. This distinction becomes visible when a fast disk feeds a slower parser. Completions arrive promptly, yet the parser still owns many payloads. A correct pool must account for those application-held buffers as well as operations still in flight. The design question is precise: which event ends each owner’s access, and who records that event?

Completion entries report operations, not a universal free command

In io_uring, submission entries describe work and completion entries report results with an application-supplied identity. Do not infer completion order from submission order unless the particular API and dependency mechanism guarantee it. Associate each operation with a stable request record. A slot number plus a generation counter is a useful identity scheme when records are recycled: a stale completion for generation seven must not be applied to the new request occupying that slot at generation eight.

Consuming a completion entry returns space in the completion ring; it does not tell your parser to stop reading the payload. Copy fields you need before advancing the ring, then advance exactly as the library contract requires. A conventional one-shot read’s terminal completion ends that operation’s kernel access, but the application may retain the returned bytes. Other operation types can produce multiple entries or extra notifications, so use their documented terminal conditions rather than assuming the first entry ends every lifetime.

N=F+I+DN=F+I+D
For this simplified one-buffer-per-read pool, N buffers are partitioned into free F, in-flight I, and completed-but-application-held D. No sharing or overlapping operations are allowed in this model.↔ Scroll wide equations horizontally.

Turn ownership into a state machine

For an ordinary read pool, write the transitions as free to submitted, submitted to application-held on terminal completion, and application-held to free after the final consumer releases it. A failed read still needs a terminal transition and cleanup; error status does not make ownership disappear. Cancellation likewise initiates another protocol step. It does not permit freeing the target operation’s memory while that target can still access it. Keep the request record until all required terminal evidence has been handled.

If several consumers share one payload, track their references explicitly or transfer ownership along a single chain. The right condition for recycling is that neither the I/O mechanism nor an application reader may still access the region. Registered buffers reduce repeated registration work, but registration is not permission to overwrite an in-flight payload. Keep registration lifetime and per-operation access lifetime separate; eventual unregistration or replacement must also follow its own documented resource-release rules.

reusable⁡(b)  ⟺  Kb=0  ∧  Ab=0\operatorname{reusable}(b)\iff K_b=0\;\land\;A_b=0
K_b counts outstanding operation access obligations and A_b application access obligations for buffer b. Zero in both is necessary for recycling; registration bookkeeping can impose additional deallocation conditions.↔ Scroll wide equations horizontally.

Provision the whole lifetime and inspect the bottleneck

Pool exhaustion can be healthy backpressure. When every buffer is owned, stop issuing reads until a consumer releases one, or apply an explicit rejection policy. Allocating unbounded replacements hides the bottleneck until memory is exhausted. Record time spent waiting for I/O separately from time spent queued for parsing. A pool larger than submission depth may be necessary, but larger is not automatically faster: it can increase retained memory and queueing delay without improving the slow stage’s throughput.

Use the queue lab to explore this bottleneck only as an analogy. Let arrivals represent completed payloads reaching a parser and service represent parser releases. Press Reset, set arrival rate to 4 and service rate to 3, predict the count after one second, then press Step twice. For an independent draining case, press Reset again, explicitly set arrival rate to 2 and service rate to 3, predict the new count, then press Step twice. Each run starts at four units. The model has no finite pool, generation tags, DMA, or ownership states. Its M/M/1 residence readout applies only under that separate stochastic model’s stable assumptions; it is not a measured buffer lifetime.

Account for eight read buffers

A pool contains eight 16 KiB buffers. Five are in flight and two are held by parsers. Three I/O operations then finish, and later parsers release four buffers. No new reads are submitted. Track availability.

  1. Initially F = 8 − 5 − 2 = 1. Seven buffers are owned, retaining 7 × 16 = 112 KiB out of the pool’s 128 KiB total capacity.

  2. Three completions move buffers from I to D: I becomes 2 and D becomes 5. F stays 1. Consuming completion entries did not make those five payloads free.

  3. Four parser releases move D to F: D becomes 1, F becomes 5, and I remains 2. The three still-owned buffers account for 48 KiB; the pool allocation remains 128 KiB.

The result

Five buffers are available for new reads. Reusable capacity, currently owned capacity, and allocated capacity are three different measurements.

Explore the model

Interactive lab

Interactive lab · queue

Completed payloads waiting for consumers

Press Reset, set arrival rate λ to 4 and service rate μ to 3, predict backlog after one second, then press Step twice. Compare a new run after Reset with both rates explicitly set to 2 and 3. Arrivals and service stand for payload production and consumer release; each fluid run starts at four units.

Completed payloads waiting for consumersFluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Current readings: Fluid backlog: 4; Net rate · λ − μ: −1 /s; M/M/1 mean residence: 1 s. All parameters are adjustable with the labeled controls below.02.557.51001.534.56Elapsed time · sFluid backlogλ < μ
Fluid backlogCurrent time
Fluid backlog
4
Net rate · λ − μ
−1 /s
M/M/1 mean residence
1 s
t = 0 s

Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.

Look forA consumer bottleneck can retain buffers after I/O completes. This unbounded fluid queue does not implement a finite buffer pool or establish any buffer’s actual ownership.

Check your understanding

An ordinary one-shot read has completed, and its completion entry has been marked consumed. A parser still holds a view of the returned payload. Which resource can be reused immediately under these facts?

Try it yourself

A steady workload completes 32,000 one-buffer reads per second. A buffer is held for an average of 2 ms from submission until its final consumer releases it. Estimate average owned buffers and memory for 4 KiB buffers. Is this estimate a safe maximum pool size?

Take a moment to make a prediction or work it out on paper. Open a hint when you need one.

Give me a hint

Multiply throughput by mean holding time in seconds. Average occupancy is not a bound on burst occupancy.

Compare with the solution

The average is 32,000 × 0.002 = 64 owned buffers, consuming 64 × 4 = 256 KiB. This is a steady-state mean under consistent measurement boundaries, not a guaranteed capacity bound. Bursts, latency tails, application stalls, and any free-buffer reserve require separate analysis. A bounded pool also needs a defined behavior when all buffers are owned.

N‾owned=r W‾=32,000 s−1×0.002 s=64\overline{N}_{\mathrm{owned}}=r\,\overline{W}=32{,}000\,\mathrm{s}^{-1}\times0.002\,\mathrm{s}=64
Little’s law connects average throughput and complete buffer holding time for a stable, consistently observed system.↔ Scroll wide equations horizontally.

Take it with you

  • A completion-ring slot and its payload buffer have different reclamation events.
  • Track request identity and every kernel or application access obligation before reuse.
  • Size pools around the complete buffer holding interval and define bounded backpressure.

Read further

Original sources and deeper explanations to follow at your own pace.

Optional progress, saved in this browser when available. Revisit any lesson at any time.

Lesson not marked complete.