---
title: "Never hold a lock while waiting on the · Agent Wranglers"
url: https://agentwranglers.se/playbook/architecture/concurrency/no-lock-across-io
id: ARC-81
level: Recommended
updated: 2026-10-22
tokens: 745
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Never hold a lock while waiting on the network

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Concurrency](https://agentwranglers.se/playbook/architecture/concurrency.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-22.

- ID: ARC-81
- Level: Recommended
- Chapter: [Concurrency](https://agentwranglers.se/playbook/architecture/concurrency.md)
- Rule: 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.
- 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.
- How: 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](https://agentwranglers.se/playbook/data/transactions-and-concurrency/outbox.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### TypeScript example

```ts
// 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

```python
# 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
```

## Related rules

- DAT-18 [Send side effects through an outbox](https://agentwranglers.se/playbook/data/transactions-and-concurrency/outbox.md) in Data and databases

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/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.

- Previous: [Take locks in one written order](https://agentwranglers.se/playbook/architecture/concurrency/lock-order.md)
- [Concurrency](https://agentwranglers.se/playbook/architecture/concurrency.md)
