ARC-81Recommended
Never hold a lock while waiting on the network
Release every lock before a network call, a slow disk read or a wait on another service. Read what you need under the lock, call outside it, and take the lock again to write.
How to do it
A lock held across a call to a provider that takes 5 seconds makes every other caller wait 5 seconds too. When the provider hangs, the whole service stops. After you take the lock again, check that the data has not changed meanwhile, or use a version number. Rust's Clippy has an await_holding_lock lint for this. Database transactions are locks too, so keep provider calls out of them, as Send side effects through an outbox says.
Examples
TypeScript example
// Bad: every caller waits for the rates provider
async function refreshRates() {
await ratesLock.runExclusive(async () => { cache.rates = await fetchRates(); });
}
// Good: fetch outside the lock, write under it
async function refreshRates() {
const fresh = await fetchRates();
await ratesLock.runExclusive(() => { if (fresh.asOf > cache.rates.asOf) cache.rates = fresh; });
}
Python example
# Good: fetch outside the lock, write under it
async def refresh_rates() -> None:
fresh = await fetch_rates()
async with rates_lock:
if fresh.as_of > cache.rates.as_of:
cache.rates = fresh
How to check
A test delays the downstream service by 5 seconds and starts a second caller that needs the same lock. The second caller finishes within 100 ms.
Related rules
- DAT-18 Send side effects through an outbox in Data and databases
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/architecture/concurrency/no-lock-across-io">Agent Wranglers clean code and architecture playbook</a>. In code you write, add a comment next to it with the same link.