Skip to main content

Resource Leak

A resource leak fails to release finite resources like file descriptors, sockets, and pooled connections, exhausting the limit so new work fails. Use scope-bound release (try-with-resources, defer, RAII), pools with limits and leak detection, and soak tests.

A resource leak is the failure to release a finite, non-memory resource — file descriptors, network sockets, database or HTTP connections, locks, threads, native handles — after acquiring it. Unlike a memory leak, the resource is usually capped by the OS or a pool, so exhaustion arrives fast and hard: once the limit is hit, every new attempt to acquire the resource fails, even though the code is otherwise correct.

Why It Happens

The acquire is obvious; the release is easy to forget, especially on error paths. A close() placed after the work but before a possible exception is skipped when that exception fires. Borrowing a connection from a pool and returning it only on the happy path leaks it on failure. Streams opened in a loop, statements not closed, locks not released in a finally — all leak slowly. Tests with a handful of iterations never exhaust the pool, so the leak ships and only surfaces after sustained production traffic.

Why It Hurts

Connection pools drain and then every request blocks or times out waiting for a connection that will never be returned. File-descriptor limits produce Too many open files and the process can no longer open sockets or files — it cannot even accept new connections. Thread leaks exhaust the scheduler. The symptom is degradation that worsens with uptime and clears only on restart, which masks the root cause and leads to scheduled restarts as a band-aid.

Warning Signs

  • open/acquire/borrow without a guaranteed matching close/release on every path.
  • Releases placed outside finally/defer/using, so exceptions skip them.
  • File-descriptor or connection counts climbing over time.
  • Pool-checkout timeouts or Too many open files after the service has been up a while.

Better Alternatives

  • Language scope guards: try-with-resources (Java), using/await using (C#), with (Python), defer Close() (Go), RAII (C++/Rust) — release deterministically on every exit.
  • Connection pools with max size, timeouts, and leak detection that reclaim or flag unreturned connections.
  • Centralized acquire/release helpers so callers cannot forget cleanup.
  • Resource limits and watchdogs that surface leaks early in testing.

How to Refactor Out of It

Wrap every acquisition in the language's scope-bound release construct so the resource is freed on normal and exceptional exit alike — never rely on a manual close after the work. Audit error paths specifically, since that is where leaks hide. Configure pools with maximum sizes, checkout timeouts, and leak detection so an unreturned connection is logged and reclaimed rather than silently lost. Add a soak test that runs many iterations and asserts file-descriptor and pool counts return to baseline. Monitor those counts in production so leaks are caught before exhaustion forces an outage.