Mesrai
Back to blog
// essayTechnical Deep Dive

Unclosed File Descriptors in a Streaming Endpoint

Real PR catch: file open without close in finally — leaks under error. try/finally + `using` fixes.

Mesrai TeamAugust 26, 20268 min read

A download endpoint that opens a file handle, streams it. No try/finally. If the stream errors, the handle never closes. After enough errors, the process hits the file-descriptor limit.

The vulnerable diff

tsapi/download.ts
// api/download.ts
downloadRouter.get("/file/:id", async (req, res) => {
  const path = await getFilePath(req.params.id);
  // BUG: no try/finally — if pipeline throws, fh.close never runs
  const fh = await fs.promises.open(path, "r");
  await pipeline(fh.createReadStream(), res);
  await fh.close();
});

What is wrong

Resource lifecycle (file descriptors, sockets, database handles, etc.) is the same pattern across every kind. Acquire, use, release. The release must happen on every exit path — success, error, and cancellation. Try/finally is the canonical structure. CWE-772 is the broader class: failure to release a resource on the error path. TC39's explicit-resource-management proposal (using keyword) gives a cleaner syntax for the same pattern.

The attack

Symptom: process gets EMFILE: too many open files after a flaky-network spell.

textexploit
# Logs:
Error: EMFILE: too many open files, uv_resident_set_memory
   at FSWatcher.start ...

# `lsof -p $PID | wc -l` shows thousands of open files for a single client connection.

Restart fixes it temporarily; the bug recurs under load.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Performance] [CWE-772] [high]

`fh.close()` not on every exit path. If `pipeline` throws, close
never runs and the file descriptor leaks.

Wrap in try/finally:

  const fh = await fs.promises.open(path, "r");
  try {
    await pipeline(fh.createReadStream(), res);
  } finally {
    await fh.close();
  }

Or use TC39 explicit-resource-management (Node 24+):

  await using fh = await fs.promises.open(path, "r");
  await pipeline(fh.createReadStream(), res);
  // fh closed at end-of-scope, even on throw

Same pattern for any resource that has an explicit close/release.

The fix

tsapi/download.ts (fixed)
// api/download.ts — fixed
downloadRouter.get("/file/:id", async (req, res) => {
  const path = await getFilePath(req.params.id);
  const fh = await fs.promises.open(path, "r");
  try {
    await pipeline(fh.createReadStream(), res);
  } finally {
    await fh.close();
  }
});

Try/finally guarantees close. The error still propagates to the framework error handler; the handle is released regardless.

Why human review missed it

FD leaks surface only under sustained error rate — dev never hits the limit. Mesrai catches every `open` / acquire-style call without a matching `close` in finally.

Related rules + further reading

Mesrai rule pack: performance/resource-close-in-finally — flags open/acquire without close in finally.

TC39: Explicit Resource Management (using keyword).

CWE-772 — Missing Release of Resource after Effective Lifetime.

Takeaway

Open in try, close in finally. Or `using` if you're on Node 24+. Mesrai catches every open without paired close.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.