Waiting Is the Problem, Not Processing
Pre-processing an article makes it open instantly, and doing it on demand makes the reader wait. The naive resolution is to process everything in advance, which is background burn on a firehose. The better one comes from watching how someone actually reads: they browse headlines first, then read. Those are separate sessions minutes apart.
So let the browsing pass mark what will be read. A queue button on the card is a tap that costs nothing at the time and authorizes work to happen before the reading pass begins. Nobody waits, nothing is spent on articles that were never chosen.
Why This Is Not an Exception to the Rule
A background worker that spends money looks like exactly what the previous lesson forbade. The distinction is who decided. Every item in the queue was put there by an explicit tap; the worker defers requested work to a convenient moment. A worker that scanned the shelf and picked promising articles itself would be the violation — same code, same cost, entirely different relationship to the person paying.
The property to preserve is that the queue is only ever populated by human action. The moment anything else can add to it, the worker becomes an autonomous spender and the rule is gone.
Two Lanes, One Claim
Two paths can now process the same article: the worker, and a direct open by someone who did not wait. They must not both pay.
The natural instinct is an in-process lock, and it does not work here — the request handler and the worker generally live in different event loops or threads, and an asyncio lock binds to the loop that first uses it, so the other side cannot wait on it. The claim has to live where both can see it, which means the database, with a timestamp so a crashed holder cannot block the article forever. The loser of the race does not fail; it polls briefly for the winner's cached result.
Finish What Was Started
One more failure mode, found in live use: a person opens an article, the processing turn begins, they close it two seconds later, and the request is cancelled — taking the turn with it. The money was spent and nothing was cached. Measured on a real day: twenty extractions, six cached.
Caching is the server's responsibility, not a side effect of the client staying connected. Running the turn shielded from cancellation means a closed reader loses the view, not the work. If you have already paid for a result, finish it and store it — the caller's interest and the value of the artifact are unrelated.