DEV Community

Cover image for The Day I Learned Node.js “Timeouts” Don’t Mean What I Thought They Meant
Frozen Blood
Frozen Blood

Posted on

The Day I Learned Node.js “Timeouts” Don’t Mean What I Thought They Meant

I used to think timeouts were simple: set a timeout, request fails after X seconds, done. Then I shipped a Node.js service that still got stuck under load even though “everything had a timeout.”

Turns out Node has multiple layers of timeouts, and if you set the wrong one (or only one), you can still end up with requests that hang, sockets that live forever, and a server that slowly stops accepting traffic.

Here’s the story — and how I fixed it.


The Symptom: “Random” Hangs Under Load

It started as a vague report:

  • “The API sometimes takes forever.”
  • “It’s not crashing, it just… stops responding.”

CPU was fine. Memory was fine. No obvious errors.

But the number of open connections kept climbing. And once it crossed a certain point, even healthy endpoints felt slow.

Classic “works until it doesn’t.”


The Mistake: I Only Set the Wrong Timeout

I had code like this:

JavaScript (Node.js):

import http from "http";

const server = http.createServer(async (req, res) => {
  // ...business logic
  res.end("ok");
});

// I thought this solved it:
server.setTimeout(10_000);

server.listen(3000);
Enter fullscreen mode Exit fullscreen mode

I believed server.setTimeout() meant:

“Kill any request that takes longer than 10 seconds.”

Nope.

That timeout is mainly about idle socket activity (and behavior differs across Node versions and request patterns). If the connection is still “active” in certain ways, you can still end up with long-lived sockets.

And even worse: my outbound calls (to other services) didn’t have proper timeouts either.


The Real Root Cause: Outbound Requests Without Timeouts

We had a dependency call like:

const r = await fetch("https://some-service/api/data");
// sometimes this never returned under network weirdness
Enter fullscreen mode Exit fullscreen mode

In the real world:

  • DNS can hang
  • TLS negotiation can stall
  • upstream can accept the connection but never respond
  • proxies can behave badly

If you don’t set timeouts on outbound calls, your handler can hang while the client connection stays open… and then your server slowly fills up with stuck requests.


The Fix: Put Timeouts at the Right Layers

I fixed it with a three-layer timeout strategy:

1) Request-level timeout (application logic)

Use AbortController so the operation stops.

JavaScript (Node.js 18+):

async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), ms);

  try {
    const res = await fetch(url, { signal: controller.signal });
    return res;
  } finally {
    clearTimeout(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now my outbound call can’t hang forever.


2) Server headers timeout (protect against slowloris / slow clients)

These are more reliable for “client is too slow” cases.

server.headersTimeout = 12_000; // must be > requestTimeout in Node
server.requestTimeout = 10_000;
Enter fullscreen mode Exit fullscreen mode

This helps when clients keep connections open and drip headers/body slowly.


3) Hard kill per request (safety net)

Even if something slips through, I wanted a final cutoff.

server.on("request", (req, res) => {
  req.setTimeout(10_000, () => {
    res.statusCode = 408;
    res.end("Request timeout");
    req.destroy();
  });
});
Enter fullscreen mode Exit fullscreen mode

This makes the behavior explicit and visible.


Bonus: The One Change That Made Debugging Obvious

I added logging on aborted requests and tracked active handles:

process.on("warning", (w) => console.warn(w.name, w.message));

setInterval(() => {
  console.log("Active handles:", process._getActiveHandles().length);
}, 10_000);
Enter fullscreen mode Exit fullscreen mode

(Yes, _getActiveHandles() is not “clean,” but for debugging it quickly tells you if your server is accumulating stuck sockets/timers.)

Once I saw handles steadily increasing, it confirmed the leak was connections waiting on something — not memory.


What I Learned

Node timeouts aren’t one thing. They’re:

  • Socket / HTTP server protection
  • Application request timeboxing
  • Outbound client timeout enforcement

If you only set one, you’ll still get weird hangs in production.


Conclusion / Key takeaway

If your Node service “hangs” under load, assume it’s not mysterious. It’s usually requests waiting forever because timeouts weren’t enforced at the right layer.

Have you ever shipped a service that didn’t crash — it just slowly stopped responding? What did it end up being?

Top comments (0)