Background Work Should Be Bounded and Supersedable
Background work should be bounded and supersedable
Background work is not a moral debt. The system does not have to finish every old job just because it started it.
If the work follows moving state, newest truth matters more than old completeness. A queue changed, a playlist refreshed, a sync cursor advanced, a cache entry was rewritten. Finishing the stale job may waste provider budget and make the app feel slower for no user-visible gain.
The rule
Background work needs both a capacity bound and a freshness rule.
The capacity bound keeps it from eating the machine. The freshness rule tells it when to stop caring.
Practical shapes
- Use finite channels for background requests.
- Process in small batches, not one giant sweep.
- Carry a generation number or revision token.
- Drop or ignore work from an old generation.
- Dedupe recent items so churn does not rewarm the same object repeatedly.
- Put timeouts around provider calls.
- Treat failure as best-effort unless the user explicitly asked for the result.
This is the background-work cousin of Bounded Concurrency First. Bounded concurrency says "do not run all of it at once." Superseding says "do not run old work just because it is still queued."
Spotuify example
When Spotuify gets a fresh playback queue, cache warming should follow that queue snapshot. If another snapshot arrives before warming finishes, the older snapshot should stop being authoritative.
That matters because music queues are alive. The user can skip, reorder, add tracks, or switch context. The daemon should warm what is likely to be useful next, not heroically complete obsolete work from five interactions ago.
The shipped queue warmer uses this shape: finite channel, generation numbers,
small batches, recent-URI dedupe, and best-effort failure handling. It warms
metadata, cover art, lyrics, index rows, and next-track audio preload only while
that work still points at the newest useful queue truth.
Mxr example
In Mxr, the old idea of automatic post-sync summary backfill was wrong for large mailboxes. One sync tick on a 100k-message account can touch far too many threads; spawning a summary job for every changed thread turns "nice to have later" into runtime pressure now.
The current shape is better: no sync-wide summary backfill. Clients request summaries on demand, and the TUI's lazy open-triggered path is debounced so arrowing through the list does not fire an LLM call for every row. If the user leaves before a result returns, the daemon can still cache it, but the TUI does not let that stale result overwrite the current view.
Provider sync needs a slightly different discipline. You cannot simply drop a
sync that is already talking to Gmail, because the cursor and provider state
matter. But you can bound it: put a timeout around provider work, release the
per-account lane, record the failure, and let the next cycle try again. See
Provider Lanes Need Account Scope.
Worth Your Time example
Worth Your Time uses the same freshness rule at two speeds. Mobile onboarding signs each film-suggestion request with the current taste description and genre selection. If the user changes either input before a response arrives, the older response cannot replace the new grid.
Taste-profile generation can run much longer, so it carries the profile version that started the job. A write is accepted only if that version is still current. The old work may finish, but it no longer owns the truth.