go-concurrency-web
Go concurrency patterns for high-throughput web applications including worker pools, rate limiting, race detection, and safe shared state management. Use when implementing background task processing, rate limiters, or concurrent request handling.
git clone --depth 1 https://github.com/existential-birds/beagle /tmp/go-concurrency-web && cp -r /tmp/go-concurrency-web/plugins/beagle-go/skills/go-concurrency-web ~/.claude/skills/go-concurrency-webSKILL.md
# Go Concurrency for Web Applications
## Quick Reference
| Topic | Reference |
|-------|-----------|
| Worker Pools & errgroup | [references/worker-pools.md](references/worker-pools.md) |
| Rate Limiting | [references/rate-limiting.md](references/rate-limiting.md) |
| Race Detection & Fixes | [references/race-detection.md](references/race-detection.md) |
## Core Rules
1. **Goroutines are cheap but not free** — each goroutine consumes ~2-8 KB of stack. Unbounded spawning under load leads to OOM.
2. **Always have a shutdown path** — every goroutine you start must have a way to exit. Use `context.Context`, channel closing, or `sync.WaitGroup`.
3. **Prefer channels for communication** — use channels to coordinate work between goroutines and signal completion.
4. **Use mutexes for state protection** — when goroutines share mutable state, protect it with `sync.Mutex`, `sync.RWMutex`, or `sync/atomic`.
5. **Never spawn raw goroutines in HTTP handlers** — use worker pools, `errgroup`, or other bounded concurrency primitives.
## Gates (check before merge or review)
Use these **sequenced** checks for objective pass/fail; do not replace them with “I verified mentally.”
1. **Race detector**
- Run `go test -race ./...` on packages that changed concurrent code, or `go build -race` for binaries under test.
- **Pass:** exit code `0`. If you report “no races,” attach or cite CI output / saved terminal transcript—do not assert cleanliness without that artifact.
2. **Bounded background work from HTTP**
- Inspect handlers and middleware that start work beyond the request goroutine.
- **Pass:** every such path uses a bounded primitive (worker pool, buffered channel with documented capacity, `errgroup` with an explicit concurrency cap)—not unbounded `go` per incoming request.
3. **Graceful teardown**
- For processes that start long-lived goroutines, trace from shutdown signal (or test `defer`) to `Wait()` / channel close / `context` cancel for each goroutine family.
- **Pass:** you can point to the call chain or a test that proves shutdown completes without hang (no orphan goroutines).
## Worker Pool Pattern
Use worker pools for background tasks dispatched from HTTP handlers. This bounds concurrency and provides graceful shutdown.
```go
// Worker pool for background tasks (e.g., sending emails)
type WorkerPool struct {
jobs chan Job
wg sync.WaitGroup
logger *slog.Logger
}
type Job struct {
ID string
Execute func(ctx context.Context) error
}
func NewWorkerPool(numWorkers int, queueSize int, logger *slog.Logger) *WorkerPool {
wp := &WorkerPool{
jobs: make(chan Job, queueSize),
logger: logger,
}
for i := 0; i < numWorkers; i++ {
wp.wg.Add(1)
go wp.worker(i)
}
return wp
}
func (wp *WorkerPool) worker(id int) {
defer wp.wg.Done()
for job := range wp.jobs {
wp.logger.Info("processing job", "worker", id, "job_id", job.ID)
if err := job.Execute(context.Background()); err != nil {
wp.logger.Error("job failed", "worker", id, "job_id", job.ID, "err", err)
}
}
}
func (wp *WorkerPool) Submit(job Job) {
wp.jobs <- job
}
func (wp *WorkerPool) Shutdown() {
close(wp.jobs)
wp.wg.Wait()
}
```
### Usage in HTTP Handler
```go
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
user, err := s.userService.Create(r.Context(), decodeUser(r))
if err != nil {
handleError(w, r, err)
return
}
// Dispatch background task — never spawn raw goroutines in handlers
s.workers.Submit(Job{
ID: "welcome-email-" + user.ID,
Execute: func(ctx context.Context) error {
return s.emailService.SendWelcome(ctx, user)
},
})
writeJSON(w, http.StatusCreated, user)
}
```
See [references/worker-pools.md](references/worker-pools.md) for sizing guidance, backpressure, error handling, retry patterns, and `errgroup` as a simpler alternative.
## Rate Limiting
Use `golang.org/x/time/rate` for token bucket rate limiting. Apply as middleware for global limits or per-IP/per-user limits.
Key points:
- Global rate limiting protects overall service capacity
- Per-IP rate limiting prevents individual clients from monopolizing resources
- Always return `429 Too Many Requests` with a `Retry-After` header
See [references/rate-limiting.md](references/rate-limiting.md) for middleware implementation, per-IP limiting, stale limiter cleanup, and API key-based limiting.
## Race Detection
Run the race detector in development and CI:
```bash
go test -race ./...
go build -race -o myserver ./cmd/server
```
The race detector catches concurrent reads and writes to shared memory. It does not catch logical races (e.g., TOCTOU bugs) or deadlocks.
See [references/race-detection.md](references/race-detection.md) for common web handler races, fixing strategies, and CI integration.
## Handler Safety
Every incoming HTTP request runs in its own goroutine. Any shared mutable state on the server struct is a potential data race.
```go
// BAD — shared state without protection
type Server struct {
requestCount int // data race!
}
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
s.requestCount++ // concurrent writes = race condition
}
// GOOD — use atomic or mutex
type Server struct {
requestCount atomic.Int64
}
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
s.requestCount.Add(1)
}
// GOOD — use mutex for complex state
type Server struct {
mu sync.RWMutex
cache map[string]*CachedItem
}
func (s *Server) handleGetCached(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
item, ok := s.cache[r.PathValue("key")]
s.mu.RUnlock()
// ...
}
```
### Rules for Handler Safety
- **Request-scoped data is safe** — `r.Context()`, request body, URL params are isolated per request.
- **Server struct fields are shared** — atag and push a release after the release PR is merged
create a release PR (auto-detects previous tag)
Guides architectural decisions for Deep Agents applications. Use when deciding between Deep Agents vs alternatives, choosing backend strategies, designing subagent systems, or selecting middleware approaches.
Reviews Deep Agents code for bugs, anti-patterns, and improvements. Use when reviewing code that uses create_deep_agent, backends, subagents, middleware, or human-in-the-loop patterns. Catches common configuration and usage mistakes.
Implements agents using Deep Agents. Use when building agents with create_deep_agent, configuring backends, defining subagents, adding middleware, or setting up human-in-the-loop workflows.
Guides architectural decisions for LangGraph applications. Use when deciding between LangGraph vs alternatives, choosing state management strategies, designing multi-agent systems, or selecting persistence and streaming approaches.
Reviews LangGraph code for bugs, anti-patterns, and improvements. Use when reviewing code that uses StateGraph, nodes, edges, checkpointing, or other LangGraph features. Catches common mistakes in state management, graph structure, and async patterns.
Implements stateful agent graphs using LangGraph. Use when building graphs, adding nodes/edges, defining state schemas, implementing checkpointing, handling interrupts, or creating multi-agent systems with LangGraph.