the tragedy of the commons
Resource Contention
Connections, threads, file handles, memory — finite pools drained by uncoordinated allocation. The computer's tragedy of the commons.
Independent actors each act in self-interest and exhaust a shared, finite resource.
01in the wild
In the wild
Connection-Pool Exhaustion
A resource grabbed and never returned drains the pool until every request hangs.
example.py
# SMELL: a connection acquired and never returned to the pool
def handler(req):
conn = pool.acquire() # under load the pool drains to zero
rows = conn.query(req.sql) # an exception here leaks the conn
return rows # ...and it is never released
# RIGHT: always return the resource, even on error
def handler(req):
with pool.acquire() as conn: # context manager releases on exit
return conn.query(req.sql)Any early return or exception between acquire and release leaks the resource. A context manager makes release unconditional.
// observed
leak: pool exhausted, every request blocks with: connection returned, throughput stable
example.java
// SMELL: close() skipped on the exception path
Connection c = pool.getConnection();
Result r = c.query(sql); // throws -> c is never closed
c.close();
// RIGHT: try-with-resources closes c no matter what
try (Connection c = pool.getConnection()) {
return c.query(sql);
}The manual close() runs only on the happy path. try-with-resources guarantees release on every path.
// observed
manual close: leaked on every thrown query try-with: released on success and failure
Unbounded Concurrency
One worker per item looks elegant until a million items melt the machine.
example.go
// SMELL: one goroutine per item -> millions of goroutines, OOM
for _, job := range jobs {
go process(job) // nothing bounds the fan-out
}
// RIGHT: a buffered channel caps concurrency
sem := make(chan struct{}, 16) // at most 16 in flight
for _, job := range jobs {
sem <- struct{}{}
go func(j Job) { defer func() { <-sem }(); process(j) }(job)
}Unbounded fan-out treats the scheduler and memory as infinite. A semaphore bounds the commons to a fair, fixed width.
// observed
unbounded: memory spikes, scheduler thrashes semaphore: steady 16-wide, predictable load
example.py
import asyncio
# SMELL: launch every task at once -> thousands of open sockets
await asyncio.gather(*(fetch(u) for u in urls)) # all 50k at once
# RIGHT: a semaphore bounds how many run concurrently
sem = asyncio.Semaphore(20)
async def bounded(u):
async with sem:
return await fetch(u)
await asyncio.gather(*(bounded(u) for u in urls))gather schedules everything immediately. Wrapping each call in a semaphore caps the in-flight set to a sustainable size.
// observed
unbounded: 'too many open files', server 503s bounded: 20 at a time, steady and polite
02cross-pollination
Where this compounds
Nondeterminism
- Pool-Exhaustion Heisenbug × Time, Money & Entropy
Data Corruption
- Signed/Unsigned Conversion Poisons a Shared Aggregate × Type Errors in Dynamic Languages
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.