Deploy Error Decoder

'Cannot set headers after they are sent' — stop the double response

Quick answer: ERR_HTTP_HEADERS_SENT means your handler sent a response and then tried to respond again - a second res.send/res.json, or a header set after the body went out. Once a response is flushed it is committed. The fix is one response per request: return every res.* call and never next() after responding.

Not your exact error? Paste it into the Deploy Error Decoder →

What the error looks like

It fires at runtime, on the request that responded twice:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (node:_http_outgoing:659:11)
    at ServerResponse.header (.../express/lib/response.js:794:10)
    at ServerResponse.send (.../express/lib/response.js:170:12)

The stack points at the second res.send/setHeader. The response was already flushed; the headers cannot change now.

Why it happens

Missing return after a response

res.send runs, code keeps going, and a later res.json responds again.

next() after responding

You sent a response and still called next(), so the next middleware or error handler responds too.

Two branches both respond

An if/else or a callback path and a fall-through that each send a response.

Async race

A timeout or error already responded, then a slow promise resolves and responds again.

Find it in three steps

1

Read the stack for the second send

# The top frames name the res.* call that ran when headers were already sent.
# That is the SECOND response - the bug is that the first one did not stop execution.
2

Hunt for a response without return

// BAD - no return, so res.json runs too:
if (!user) res.status(404).send('not found');
res.json(user);
3

Check error handlers and callbacks

// guard against responding when a response already went out
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  res.status(500).json({ error: 'internal' });
});
The real fix

Respond exactly once per request

Make every code path send one response and stop. The simplest discipline is to return every res.* call, so nothing falls through, and never call next() after you have responded.

// GOOD - return on every response, one path responds
app.get('/u/:id', async (req, res) => {
  const user = await find(req.params.id);
  if (!user) return res.status(404).send('not found');
  return res.json(user);
});

For async work, make sure a timeout/error path and the success path cannot both fire - track whether you have responded, or use a framework that does (e.g. return the value in a handler that auto-responds).

How Infraveil handles this

See the request that misbehaved

Double-response bugs are hard to reproduce because they depend on a specific path or race. On your own servers, Infraveil traces requests through your backend, so the offending request shows up with its route and context in one view - and recovery stays approval-gated and recorded, on infrastructure you control.

Request tracing surfaces the route and path that double-responded
Errors and their request context in one view, on servers you control
Recovery approval-gated and recorded

Frequently asked questions

What causes 'Cannot set headers after they are sent'?

Your code sent a response and then tried to respond again - a second res.send/res.json, or a header set after the body went out. The response is already flushed, so the second attempt throws ERR_HTTP_HEADERS_SENT.

How do I find where it responds twice?

The stack trace points to the second res.* call. Look for a res.send/res.json without a return, an if/else where both branches respond, or next() called after a response.

Does this crash the whole server?

It throws for that one request. A global error handler contains it rather than crashing the process, but the client may get a wrong or truncated response - it is a real bug to fix, not just noise.

How do I prevent it?

Respond exactly once: return every res.* call, never call next() after responding, and guard error handlers with if (res.headersSent) return next(err).

Free tools for this

Client-side, no signup — they run in your browser.