This skill should be used when the user asks to "add background processing", "cache this data", "run this async", "handle concurrent requests", "manage state across requests", "process jobs from a queue", "this GenServer is slow", or mentions GenServer, Supervisor, Agent, Task, Registry, DynamicSupervisor, handle_call, handle_cast, supervision trees, fault tolerance, "let it crash", or choosing between Broadway and Oban.
View on GitHubgeorgeguimaraes/claude-code-elixir
elixir
January 23, 2026
Select agents to install to:
npx add-skill https://github.com/georgeguimaraes/claude-code-elixir/blob/main/plugins/elixir/skills/otp-thinking/SKILL.md -a claude-code --skill otp-thinkingInstallation paths:
.claude/skills/otp-thinking/# OTP Thinking
Paradigm shifts for OTP design. These insights challenge typical concurrency and state management patterns.
## The Iron Law
```
GENSERVER IS A BOTTLENECK BY DESIGN
```
A GenServer processes ONE message at a time. Before creating one, ask:
1. Do I actually need serialized access?
2. Will this become a throughput bottleneck?
3. Can reads bypass the GenServer via ETS?
**The ETS pattern:** GenServer owns ETS table, writes serialize through GenServer, reads bypass it entirely with `:read_concurrency`.
**No exceptions:** Don't wrap stateless functions in GenServer. Don't create GenServer "for organization".
## GenServer Patterns
| Function | Use For |
|----------|---------|
| `call/3` | Synchronous requests expecting replies |
| `cast/2` | Fire-and-forget messages |
**When in doubt, use `call`** to ensure back-pressure. Set appropriate timeouts for `call/3`.
Use `handle_continue/2` for post-init work—keeps `init/1` fast and non-blocking.
## Task.Supervisor, Not Task.async
`Task.async` spawns a **linked** process—if task crashes, caller crashes too.
| Pattern | On task crash |
|---------|---------------|
| `Task.async/1` | Caller crashes (linked, unsupervised) |
| `Task.Supervisor.async/2` | Caller crashes (linked, supervised) |
| `Task.Supervisor.async_nolink/2` | Caller survives, can handle error |
**Use Task.Supervisor for:** Production code, graceful shutdown, observability, `async_nolink`.
**Use Task.async for:** Quick experiments, scripts, when crash-together is acceptable.
## DynamicSupervisor + Registry = Named Dynamic Processes
DynamicSupervisor only supports `:one_for_one` (dynamic children have no ordering). Use Registry for names—never create atoms dynamically:
```elixir
defp via_tuple(id), do: {:via, Registry, {MyApp.Registry, id}}
```
**PartitionSupervisor** scales DynamicSupervisor for millions of children.
## :pg for Distributed, Registry for Local
| Tool | Scope | Use Case |
|------|-------|----------|
| Registry | Single