Asynchronous resource leaks
Trace ownership through cancellation and shutdown, distinguish active resources from leaks, and test whether an asynchronous service reaches quiescence.
On this page
The question for today
What you’ll understand
- Give every task, connection, callback, and buffer an explicit owner and release condition.
- Separate cancellation requests from completed cleanup and safe reclamation.
- Use lifecycle accounting and quiescence tests to investigate resource growth.
An asynchronous operation can finish from the caller's perspective while leaving work behind: a socket draining, a callback registered, a timer retaining a request, or a task whose exception nobody observes. Such resources are harder to notice than an ordinary missing close because ownership crosses suspension points. The useful question is not merely whether memory rose during load. Ask which owner still needs each resource, what event ends that obligation, and how shutdown proves that those events have actually occurred.
Write the ownership ledger before the cleanup code
Treat each acquired resource as an obligation. Name the owner, the operations that may borrow it, and the transition that releases it. A connection handler may own its stream while a pending write borrows a buffer; a timeout task may borrow the handler but must not keep it alive after termination. Transfer ownership explicitly when placing work into a queue. Returning an error to the request owner does not automatically discharge obligations held by background operations.
Use counts together with state: active requests, open descriptors, pending tasks, registered timers, and bytes retained in queues. Rising counts under rising concurrency can be legitimate; counts that remain after all admitted work has ended deserve investigation. Pool capacity and allocator caches can also retain memory intentionally. Compare a quiet baseline with the same quiet state after repeated load cycles, and keep a small inventory of resource identities so a graph of total memory does not become your only evidence.
Cancellation requests need a joining boundary
Requesting cancellation is a control signal, not proof that a task has exited. Cooperative cleanup may need another scheduling turn, and some operations can complete despite a cancellation race. Keep resources alive until the API guarantees that no pending operation can access them. Conversely, do not wait forever for a task that has lost every path to completion. Give shutdown stages deadlines, record unresolved ownership, and choose an explicit final escalation policy appropriate to the process.
Structured task scopes make the joining boundary visible: a parent retains responsibility for children and observes their completion or failure before leaving the scope. Put release logic on success, exception, and cancellation paths, and preserve the runtime's cancellation contract after cleanup. Observe background task failures rather than allowing them to disappear. In Python streams, initiating closure and awaiting its completion are distinct operations. This example illustrates lifecycle separation; other runtimes may expose different completion or reclamation guarantees.
Test quiescence, including the unhappy paths
A shutdown protocol can stop admission, notify producers, drain or reject queued work, request cancellation where needed, join owned tasks, complete stream closure, unregister callbacks, and release final buffers. The exact order follows dependencies: a consumer cannot drain if you destroyed its required service first. Define quiescence in observable terms, such as no owned tasks and no open request connections, while documenting intentional process-wide resources. An idle event loop alone proves neither that resources were released nor that remote work stopped.
Inject faults before registration, after queue insertion, during a partial operation, and during cleanup. Repeat the workload and verify return to the declared baseline. In the queue lab, press Reset, set arrival rate to 4 and service rate to 3, predict growth, then press Play. For a separate release-dominant comparison, press Reset again, explicitly set arrival rate to 2 and service rate to 3, predict draining, then press Play. Both runs start with four abstract items rather than carrying a previous backlog forward. The fluid model illustrates conservation pressure only; it does not model task ownership, garbage collection, cancellation races, or prove that your implementation is leak-free.
Work it through
Account for retained request buffers
A test processes exactly 100 completed requests per second for 60 seconds. Exactly 1% of those requests accidentally retain one unique 16 KiB buffer. The quiet baseline contains eight such buffers; no retained buffer is reclaimed during this test.
The run completes 100 × 60 = 6,000 requests. One percent is sixty retained buffers; the other 5,940 request buffers are released after their work ends.
The additional retained memory is 60 × 16 = 960 KiB. Baseline retention is 8 × 16 = 128 KiB, so this narrowly defined buffer inventory now occupies 1,088 KiB.
Stop admission and join the request work. If the inventory remains at sixty-eight buffers instead of returning to the documented baseline of eight, inspect owners of the extra sixty and the release transition they missed.
The added inventory grows at one buffer per second in this constructed trace. Its persistence after quiescence identifies a lifecycle defect; total process memory alone would not establish the same conclusion.
The living notebook
Explore the model
Interactive lab · queue
Acquisitions versus completed releases
Treat equal-sized abstract items as live resource obligations. Press Reset, set arrival rate to 4 and service rate to 3, predict growth, then press Play. Compare a fresh run after another Reset with both rates explicitly set to 2 and 3; predict draining before Play. Each run starts at four items.
- Fluid backlog
- 4
- Net rate · λ − μ
- −1 /s
- M/M/1 mean residence
- 1 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 forThis fluid analogy illustrates an accounting imbalance, not ownership or leak detection. Ignore the separate M/M/1 residence readout for this interpretation; it is a different stochastic queue model.
Pause & reason
Check your understanding
Your turn
Try it yourself
A five-minute test completes 40 requests per second. Exactly 0.5% retain one unique 32 KiB buffer, with no later reclamation. Compute the extra retained count and memory. Describe one observation needed before calling the growth a leak.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Convert five minutes to seconds, then apply the retained fraction to completed requests. Compare equivalent quiet states rather than comparing an idle process with a busy process.
Compare with the solution
The test completes 40 × 300 = 12,000 requests; 0.005 × 12,000 = sixty extra buffers retain 1,920 KiB. Stop new work and wait for the documented joining and closure boundaries. If the extra buffer identities remain beyond their owners' intended lifetimes rather than returning to the declared quiet baseline, investigate the missing release path. Intentional caches or a still-draining operation require a different interpretation, so the arithmetic is a test hypothesis, not a standalone proof.
Keep the idea
Take it with you
- Every asynchronous resource needs an owner, borrowers, and a defined release boundary.
- Cancellation requests and initiated closure are distinct from completed cleanup.
- Compare repeated equivalent quiet states and inspect identities to distinguish leaks from legitimate retention.
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.