- Published on
An Arc clone can keep old data alive longer than you expect
- Authors

- Name
- Mehdi Akiki
Article · Interrupted execution
The configuration reload succeeds. The application replaces its current snapshot, but memory usage stays high.
There may be no leak. Tasks that started before the reload still own the old snapshot. A queued task can retain it even before doing useful work.
Arc makes sharing easy. That convenience can make the ownership graph easy to overlook.
The handle is small; its responsibility may be large
Rust's Arc documentation explains that cloning creates another reference to the same allocation. It does not copy the whole payload. The payload remains until its last strong owner is gone.
A small Rust example makes the retention visible:
use std::sync::Arc;
let current = Arc::new(vec![0_u8; 1024 * 1024]);
let queued_work = Arc::clone(¤t);
let old = Arc::downgrade(¤t);
drop(current);
assert!(old.upgrade().is_some());
drop(queued_work);
assert!(old.upgrade().is_none());
There is one payload allocation here. The queued owner keeps it alive after the current owner disappears. The Weak handle lets the example ask whether a strong owner remains without becoming a persistent strong owner itself.
The future-size article measures values stored inside suspended futures. Do not read the size of an Arc or Vec handle as the total memory that a task retains. Inline state and reachable heap allocations answer different questions.
Decide when a task needs a snapshot
Some tasks need the exact configuration they were created with. An invoice calculation may require a specific rules version for reproducibility. Retaining that snapshot is correct, and the memory budget must include it.
Other tasks only need the latest routing configuration when they begin. Capturing a large snapshot when enqueuing them can retain many obsolete versions for no benefit. Store the necessary job input and acquire the appropriate snapshot at execution time instead.
That changes semantics, so do it deliberately. “Latest when queued,” “latest when started,” and “fixed for the whole operation” are different contracts. A memory optimization should not quietly switch between them.
Bound queue length and task duration as well. Faster reloads combined with slow work can keep several generations alive at once. A timeout or cancellation path that forgets a task handle may extend retention further.
Measure owners and payloads separately
For a reproduction, attach a drop counter to a snapshot and hold workers at a barrier. Replace the current snapshot several times, then release workers in groups. Check when each generation is actually dropped.
This demonstrates ownership. It does not establish exact process memory use: allocators may retain freed pages, and a shared allocation must not be counted once per Arc clone.
Use allocation or heap tools when the production question is retained bytes. Use an ownership trace when the question is which task keeps a generation alive. Both can be useful, but they are not interchangeable measurements.
Weak references can help when a task is allowed to discover that its data has gone away. They are not a universal replacement for strong ownership. If the operation requires that snapshot to finish correctly, keep it and budget for it. The improvement is knowing why the old data remains, rather than assuming a cheap clone has no lifetime cost.
Hold two old generations at once
The service-style ownership test gives each snapshot a generation number and a destructor counter. A task captures generation zero, the current snapshot is replaced, another task captures generation one, and the current snapshot moves to generation two.
Both old generations now have one strong owner: their respective queued tasks. The test keeps only Weak observers outside those tasks. It releases the tasks separately and checks each generation's destructor counter and Weak upgrade result. Finishing generation zero must not release generation one. Each payload destructor must run exactly once.
The current generation has its own final release. Dropping it increments generation two's counter independently. This makes the ownership graph visible without adding together several Arc handles as if each represented another full copy of the payload.
The test uses small fixed payloads and explicit release channels. It does not sample RSS, allocator pages, or the memory cost of a production configuration. A destructor running proves that Rust released the payload's ownership. It does not promise that the operating system immediately sees a lower resident-set figure.
A queued snapshot can be intentional. Jobs that must use a particular rules version should retain it and record that version in their outcome. Jobs that may use the latest version can defer acquisition until execution. Choose that semantic rule first, then use the generation counters to check that the implementation follows it. The ownership evidence helps explain retention; it does not choose the product's consistency contract for you.