Why am I getting "H12 Request timeout" errors in NodeJS?


The Heroku Dashboard Metrics and my application logs are showing H12 "Request timeout" errors. In some cases, there is no debugging information, even when using New Relic, Trace or OpBeat.


In the general case (H12s caused by long running actions)

With H12 - Request Timeout errors, we generally see this pattern where one long-running action starts hogging the queue which in turn affects any subsequent requests.

Our router will drop a long-running request after 30 seconds, but the dyno behind it will continue processing the request until completion. Our router is unaware of it, though, so it'll dispatch new requests to that busy dyno. This effect tends to compound, and you'll eventually see H12 errors even for unrelated URLs, such as static assets. H13 errors are similar in what causes them, but are primarily related to concurrent web servers.

You'll need to use a tool like New Relic to gain visibility into queueing in your app.

If your app is using ExpressJS, you will also want to install something like timeout, which will ensure that a long running request is dropped at the dyno-level as well. Specifically, timeout raise a Response timeout exception when that happens.

With that in place, the compound effect is less likely to occur, but long-running actions still need to be addressed. Again, New Relic is a great tool to provide the visibility into your app to identify the long-running actions. You can then optimize them and make sure they're able to finish within a reasonable time, we suggest keeping all requests under 500ms. If they're performing any inherently long tasks, you should try to offload those to a background worker.

In the case where no debugging information is showing up in monitoring tools

This section refers to an example application from here.

Node doesn't implicitly handle requests by sending responses - that responsibility is up to you as the developer. A common mistake in node applications is to provide responses in some branches, but leave out responses in other logical branches.

In this server, routes with 'works' in their url are handled, but routes that fail the if test are left to linger.

Noticing that the app hasn't sent a response within 30 seconds, Heroku's router triggers an H12.

How to fix it

If your app triggers H12s, the first thing you should do is add logs along every branch of the failing route (every if statement, function call, etc). It's best if these logs include the x-request-id header so you can easily grep for a single request thread.

Then, make a request to that route and see where the request ends in the logs. What is the last successful step?

Finally, investigate the code to find which branch doesn't provide a response.

With Express, following the chain-of-responsibility pattern can help minimize this sort of logical error. Instead of this:

app.get('/app/:id', doEverythingInOneHugeFunctionWithAsyncBranches);

Use this:

app.get('/app/:id', checkUserAuth, findApp, renderView, sendJSON);

function checkUserAuth(req, res, next) {
  if (req.session.user) return next();
  return next(new NotAuthorizedError());

You'll also get the added benefit of unified error handling through the error middleware stack.