Memory Bugs

5.A Tap Left Running Since Morning

A

In this chapter

We'll learn why memory bugs happen and how to recognise the main ones — memory leaks (taps left running) and dangling references (a key to a room that's been given to someone else) — then find and fix the 3 AM leak and prove it with the memory graph.

14–16 min

The Problem in Real Life

Anna knows the cause. Now she has to fix it — and prove it's fixed, because "it didn't crash last night" isn't proof. The crash might just be waiting for a busier day.

John pins two printed graphs on the wall. One is a slow, steady climb to 2 GB. The other is a saw: up, down, up, down, always coming back to the same level. "By the end of today," he says, "your service should look like the saw."

J

Healthy memory breathes in and out. Leaking memory only breathes in.

John

"It Crashed, Restart It" vs. Finding What's Holding On

Leaks grow slowly

A leak isn't one big mistake — it's a tiny one, repeated thousands of times until memory runs out.

The graph tells the story

The shape of the memory graph shows whether memory is being given back.

Restarting hides the problem

Restarts reset the memory, but the leak starts again immediately.

Memory Bugs: Leaks and Dangling References

Why memory bugs happen: memory is invisible, it works perfectly on small tests, and problems only appear after hours or days of real use. A leak of 5 KB per request is invisible in a 10-minute test and fatal after 300,000 requests. That's why memory bugs are some of the trickiest to find — and why reading the memory graph is so useful.

  • Memory leak — a tap left running: imagine leaving a tap dripping in a sink with no drain. Each drip is tiny. But after a day, the sink overflows. A memory leak is memory that's no longer needed but never given back. Each leaked piece is small; the program keeps leaking until it runs out of memory and crashes — or gets slower and slower first, as the garbage collector works harder and harder.
  • Common leaks in garbage-collected languages: a global list, map or cache that only ever grows; timers or intervals that are never cleared; event listeners added again and again and never removed; closures that capture big objects and are kept forever. All the same shape: something reachable keeps holding references to things you're finished with.
  • Dangling reference — a hotel key after checkout: imagine you still have a key card to room 512 after you checked out — and the hotel has already given the room to someone else. If you use the key, you walk into a stranger's room. A dangling reference (or dangling pointer) points to memory that has already been freed and maybe reused for something else. In C and C++ this is the classic use-after-free bug: the program reads garbage, crashes, or — worse — becomes a security hole (Act 22). Garbage-collected languages largely prevent it, because memory is never freed while something still references it.
  • The close cousin in JavaScript — a stale reference: you can still hold an outdated object: for example, keeping an old copy of a seat object after the real seat changed. It's not a crash, but the code acts on old data. The fix is the same idea: don't hold on to things longer than you need them.
Table — Memory bugs at a glance
BugEveryday pictureWhat happensLanguages
Memory leakA tap left runningMemory grows until the program slows or crashesAll languages
Dangling reference (use-after-free)A hotel key after checkoutReads freed or reused memory: crashes, wrong data, security holesMainly C and C++
Stale referenceAn old phone number for a friend who movedCode acts on outdated dataAll languages
Stack overflowA pile of trays hitting the ceilingCrash from too-deep recursionAll languages
Table — Common leak patterns and fixes (garbage-collected languages)
Leak patternFix
Global map or list that only growsRemove entries when done, or limit its size
Timers or intervals never clearedclearTimeout / clearInterval when no longer needed
Event listeners added again and againRemove listeners, or add them only once
Cache without a size or time limitEvict old entries (size limit or expiry)
Closures capturing large objects, kept foreverCapture only what's needed; release the function
Table — The 3 AM fix, measured in a load test
VersionMemory after 1 hour of simulated holdsGraph shape
BeforeOver 1.5 GB and still climbingRising floor — leak
AfterBetween 190 and 240 MB, steadySaw — healthy
This whole line is a row — one record

Reading the memory graph

Healthy: a saw

up and down, always back to the same level

Leaking: a rising floor

each low point a little higher than the last

why

Garbage collector frees memory

nothing is holding old objects

Garbage collector can't free it

something reachable still references old objects

result

Runs for months

Crashes: heap out of memory

after hours or days

The fix: let go of timers when a hold ends
const timers = new Map();
function holdSeat(seatId, fanId) {
const timer = setTimeout(() => endHold(seatId, "expired"), HOLD_DURATION_MS);
timers.set(seatId, timer);
// ...
}
// Called when a hold ends for ANY reason: paid, released or expired
function endHold(seatId, reason) {
clearTimeout(timers.get(seatId)); // stop the timer if it's still waiting
timers.delete(seatId); // remove the reference -> the GC can free it
// ...update the seat's state (SOLD or FREE) depending on reason
}

The bug was never the timer itself. It was the map holding on to every timer forever. One delete per hold turns a rising floor into a saw.

Reading the memory graph: a healthy program's memory looks like a saw: it rises as work creates objects, then drops each time the garbage collector cleans up, always returning to roughly the same level. A leaking program's memory also has small teeth, but the bottom of each tooth is a little higher than the last — the whole line slowly climbs. That rising floor is the signature of a leak.

Fixing the 3 AM leak: Anna makes two changes. First, every time a hold ends — paid, released, or expired — the service clears that seat's timer and removes it from the map: clearTimeout(timer) and timers.delete(seatId). Now nothing keeps old timers reachable, and the garbage collector can do its job. Second, she remembers Act 08's design: instead of 300,000 separate timers, one small job could run every minute and release all expired holds. She adds a note to the backlog to switch to that simpler design later.

Proving the fix: she runs a load test that simulates a full day of holds in an hour, and watches the graph. Before the fix: a steady climb past 1.5 GB. After: a saw between 190 and 240 MB, perfectly flat across the whole hour. That night, and every night after, the 3 AM alert stays quiet.

Key Takeaway

A memory leak is like a tap left running: memory no longer needed but never released, tiny each time until the program runs out. In garbage-collected languages it's always something reachable — a global map, a timer, a listener, a closure — holding references too long. A dangling reference is a key to a room already given away (use-after-free in C/C++). Healthy memory graphs look like a saw; a rising floor means a leak.

Why This Matters

Memory leaks are one of the most common causes of services that "randomly" crash at night, slow down over days, or need regular restarts. On Sale Day, 50,000 fans will create holds in an hour — the old leak would have crashed the service in minutes. Knowing the leak patterns, and how to read a memory graph, lets Anna catch problems like this long before customers notice.

The 3 AM alert is quiet. John has one last exercise before Act 11: a different service, a different graph, and a small piece of code with a leak hidden in it.

Next