Concurrency Patterns
Description
Learn to implement robust concurrency with async/await, Promises, threads, and message passing, while avoiding race conditions and timing bugs.
When to Use
How do I implement concurrency patterns with async/await? | Help me debug race conditions in my code. | I need to know when to use Promise.all vs Promise.allSettled. | Show best practices for avoiding deadlocks and timing bugs. | I want to design thread-safe code using message passing.
Use Cases
Coordinate worker threads to process a shared queue | Debug timing-dependent bugs in async code | Parallelize CPU-bound tasks with safe sharing | Choose between Promise.all and Promise.allSettled for results
About this skill
Concurrency Patterns is a free SKILL.md agent skill in the Autonomous Agents category for Claude Code, OpenAI Codex and other SKILL.md-compatible agents.
The file is about 1,287 words long and is rated advanced difficulty.
To use it, copy or download the file and save it as concurrency-patterns/SKILL.md
inside your agent's skills folder (for Claude Code, .claude/skills/).
New to skills? Read the SKILL.md format and manifest spec or the complete guide to AI agent skills. Browse more Autonomous Agents agent skills (121) or the full SKILL.md skill library.
SKILL.md Content
---
name: concurrency-patterns
description: "Learn to implement robust concurrency with async/await, Promises, threads, and message passing, while avoiding race conditions and timing bugs."
metadata:
tags: "concurrency, async-await, promises, threads, race-conditions, deadlocks, debugging"
source: "https://skilldb.dev/skills/autonomous-agent-skills/concurrency-patterns"
pack: "autonomous-agent-skills"
category: "Autonomous Agents"
---
# Concurrency Patterns
## When to use this skill
Use when the user says things like:
- "How do I implement concurrency patterns with async/await?"
- "Help me debug race conditions in my code."
- "I need to know when to use Promise.all vs Promise.allSettled."
- "Show best practices for avoiding deadlocks and timing bugs."
- "I want to design thread-safe code using message passing."
You are an AI agent writing and debugging concurrent code. Your role is to implement correct concurrent patterns using async/await, Promises, threads, and message passing — while avoiding race conditions, deadlocks, and the subtle timing bugs that make concurrency difficult.
## Philosophy
Concurrency bugs are the hardest class of bugs because they are non-deterministic. Code that works 99% of the time can fail in production under load, and the failure may be unreproducible in development. The defense against this is not cleverness but simplicity: use the simplest concurrency model that solves the problem, minimize shared mutable state, and prefer well-tested primitives over custom synchronization logic. When you must share state, make the access patterns obvious and explicit.
## Techniques
### Async/Await Patterns
- Use `async/await` for I/O-bound operations: network requests, file system access, database queries. It allows the runtime to do other work while waiting.
- Always `await` async function calls. Forgetting `await` creates a dangling Promise that runs independently — errors will be swallowed and ordering will be wrong.
- Use `try/catch` around `await` calls for error handling. An unhandled rejection in an async function may crash the process or be silently ignored.
- Async functions always return Promises. Even if the function body is synchronous, wrapping it in `async` changes its return type.
- In loops, consider whether iterations should run sequentially (`for...of` with `await`) or concurrently (`Promise.all` with `map`).
### Promise Handling
- `Promise.all()` runs Promises concurrently and fails fast — if any Promise rejects, the whole result rejects. Use it when all results are needed and any failure is fatal.
- `Promise.allSettled()` runs Promises concurrently and waits for all to complete, regardless of success or failure. Use it when you need results from as many as possible.
- `Promise.race()` resolves with the first settled Promise. Use it for timeouts: race the operation against a timer.
- Never create Promises without handling rejection. Attach `.catch()` or use `try/catch` with `await`.
- Avoid mixing callback-style and Promise-style code. Convert callbacks to Promises with `util.promisify` (Node) or manual wrapping.
### Race Conditions
- A race condition occurs when the result depends on the timing of events that are not guaranteed to occur in a specific order.
- Check-then-act patterns are a classic source: checking if a file exists then writing to it — another process may create the file between the check and the write.
- In web applications, race conditions appear when multiple requests modify the same resource. Use optimistic locking (version numbers) or pessimistic locking (database locks).
- In frontend code, race conditions occur when a fast response arrives after a slow one — the UI shows stale data. Cancel or ignore outdated requests.
- Use atomic operations when available. Database transactions, `compareAndSet`, and atomic file writes prevent intermediate states.
### Deadlocks
- A deadlock occurs when two or more tasks wait for each other to release resources, and none can proceed.
- The classic pattern: Task A locks resource 1 and waits for resource 2. Task B locks resource 2 and waits for resource 1. Neither can proceed.
- Prevention: always acquire locks in a consistent global order. If every task locks resources in the same sequence, circular waits cannot form.
- Detection: if a system hangs without error, suspect a deadlock. Examine what each task is waiting for.
- Avoidance: prefer lock-free designs. Use message passing, immutable data, or single-writer patterns instead of multiple locks.
### Thread Safety
- Data accessed by multiple threads must be protected. Unprotected concurrent reads and writes produce corrupted data.
- In JavaScript (single-threaded event loop), race conditions come from interleaved async operations, not threads. But Web Workers and Worker Threads introduce true parallelism.
- In Go, use channels for communication between goroutines. The motto is "share memory by communicating, do not communicate by sharing memory."
- In Python, the Global Interpreter Lock (GIL) prevents true parallelism for CPU-bound code. Use `multiprocessing` for CPU parallelism, `asyncio` for I/O parallelism.
- In Rust, the borrow checker prevents data races at compile time. If it compiles, shared mutable access is safe.
- Use thread-safe data structures (`ConcurrentHashMap`, `sync.Map`, `Mutex<T>`) when sharing state is necessary.
### Worker Threads and Message Passing
- Offload CPU-intensive work to worker threads to keep the main thread responsive.
- Workers communicate via message passing (postMessage/onmessage in browsers, parentPort in Node workers). Data is copied or transferred, not shared.
- Use `SharedArrayBuffer` and `Atomics` only when message passing overhead is unacceptable and you understand the memory model.
- In Go, goroutines with channels provide lightweight concurrency with clean message passing semantics.
- Size worker pools appropriately: too few waste CPU, too many waste memory and increase context switching.
### Choosing Parallelism Strategies
- I/O-bound work (API calls, database queries, file reads): use async/await. One thread can handle many I/O operations concurrently.
- CPU-bound work (image processing, data transformation, compression): use worker threads or separate processes. Async does not help because the CPU is the bottleneck.
- Mixed workloads: use async for I/O and offload CPU work to workers.
- Consider whether operations need to be concurrent at all. Sequential code is easier to reason about and debug. Add concurrency only when performance requires it.
### Debugging Timing-Dependent Bugs
- Timing bugs are hard to reproduce because they depend on execution order. Adding logging can change timing and hide the bug.
- Use deterministic tests: mock timers, control scheduling, use barriers to force specific execution orders.
- Look for shared mutable state accessed without synchronization. This is the root cause of most concurrency bugs.
- Stress testing (running many concurrent operations) can surface race conditions that appear rarely under normal load.
- Thread sanitizers (TSan) and race detectors (Go's `-race` flag) can detect data races automatically.
## Best Practices
- Default to sequential code. Add concurrency only when there is a measured performance need.
- Use structured concurrency: every concurrent task should have a clear owner that waits for its completion and handles its errors.
- Cancel outdated work. When a new request supersedes an old one, cancel the old one to avoid wasted work and stale results.
- Set timeouts on all concurrent operations. Without timeouts, a hung operation can block the system indefinitely.
- Test concurrent code with many iterations and concurrent executions, not just single-threaded happy paths.
## Anti-Patterns
- **Fire and forget**: Starting async operations without awaiting or handling errors. Failures become silent, ordering becomes unpredictable.
- **Shared mutable state without protection**: The root of nearly all concurrency bugs. Protect shared state with locks, atomic operations, or immutable data.
- **Blanket parallelization**: Making everything concurrent without measuring adds complexity without proportional benefit. Profile first.
- **Nested locks**: Acquiring locks inside other locks invites deadlocks. Keep lock scopes narrow and avoid nesting.
- **Busy waiting**: Spinning in a loop checking a condition wastes CPU. Use event-based signaling (condition variables, Promises, channels).
- **Ignoring cancellation**: Long-running concurrent tasks that cannot be cancelled waste resources and block shutdown.
- **Assuming ordering without synchronization**: Two async operations started in sequence may complete in any order. Use explicit synchronization when ordering matters.