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

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.