A Complete Guide to Timeouts in Node.js

Better Stack Team
Updated on July 2, 2022

The idea behind timeouts is that in scenarios where a program has to wait for something to happen (such as a response to an HTTP request), the waiting is aborted if the operation cannot be completed within a specified duration. This allows for a more efficient control of sever resources as stuck operations or stalling connections are not allowed continued use of limited resources.

When writing servers in Node.js, the judicious use of timeouts when performing I/O operations is crucial to ensuring that your application is more resilient to external attacks driven by resource exhaustion (such as Denial of Service (DoS) attacks) and Event Handler Poisoning attacks. By following through with this tutorial, you will learn about the following aspects of utilizing timeouts in a Node.js application:

  • Enforcing timeouts on client connections.
  • Canceling outgoing HTTP requests after a deadline.
  • Adding timeouts to Promises.
  • A strategy for choosing timeout values.

Prerequisites

To follow through with this tutorial, you need to have the latest version of Node.js installed on your computer (v18.1.0 at the time of writing). You should also clone the following GitHub repository to your computer to run the examples demonstrated in this tutorial:

git clone https://github.com/betterstack-community/nodejs-timeouts
Copied!

After the project is downloaded, cd into the nodejs-timeouts directory and run the command below to download all the necessary dependencies:

npm install
Copied!

Timeouts on incoming HTTP requests (Server timeouts)

Server timeouts typically refer to the timeout applied to incoming client connections. This means that when a client connects to the server, the connection is only maintained for a finite period of time before it is terminated. This is handy when dealing with slow clients that are taking an exceptionally long time to receive a response.

Node.js exposes a server.timeout property that determines the amount of inactivity on a connection socket before it is assumed to have timed out. It is set to 0 by default which means no timeout, giving the possibility of a connection that hangs forever.

To fix this, you must set server.timeout to a more suitable value:

http_server.js
const http = require('http');
const server = http.createServer((req, res) => {
  console.log('Got request');

  setTimeout(() => {
    res.end('Hello World!');
  }, 10000);
});

server.timeout = 5000;

server.listen(3000);
Copied!

The above example sets the server timeout to 5 seconds so that inactive connections (when no data is being transferred in either direction) are closed once that timeout is reached. To demonstrate a timeout of this nature, the function argument to http.createServer() has been configured to respond 10 seconds after a request has been received so that the timeout will take effect.

Go ahead and start the server, then make a GET request with curl:

node server_example1.js
Copied!
curl http://localhost:3000
Copied!

You should see the following output after 5 seconds, indicating that a response was not received from the server due to a closed connection.

Output
curl: (52) Empty reply from server

If you need to do something else before closing the connection socket, then ensure to listen for the timeout event on the server. The Node.js runtime will pass the timed out socket to the callback function.

. . .
server.timeout = 5000;

server.on('timeout', (socket) => {
console.log('timeout');
socket.destroy();
});
. . .
Copied!

Ensure to call socket.destroy() in the callback function so that the connection is closed. Failure to do this will leave the connection open indefinitely. You can also write the snippet above as follows:

server.setTimeout(5000, (socket) => {
  console.log('timeout');
  socket.destroy();
});
Copied!

This method of setting server timeouts also works with Express servers:

express_server.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  console.log('Got request');
  setTimeout(() => res.send('Hello world!'), 10000);
});

const server = app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

server.setTimeout(5000, (socket) => {
  console.log('timeout');
  socket.destroy();
});
Copied!

If you want to override the server timeout on a particular route, use the req.setTimeout() method as shown below:

app.get('/', (req, res) => {
  req.setTimeout(20000);
  console.log('Got request');
  setTimeout(() => res.send('Hello world!'), 10000);
});
Copied!

This will cause requests to the site root to timeout after 20 seconds of inactivity instead of the 5 second default.

Timeouts on outgoing HTTP requests (Client timeouts)

Setting timeouts on outgoing network requests is a crucial requirement that must not be overlooked. Networks are unreliable, and third-party APIs are often prone to slowdowns that could degrade your application's performance significantly. That's why you should never send out a network request without knowing the maximum time it will take. Therefore, this section will discuss how to set timeouts on outgoing HTTP requests in Node.js.

Setting timeouts on the native request module

The native http.request() and https.request() methods in Node.js do not have default timeouts nor a way to set one, but you can set a timeout value per request quite easily through the options object.

request.js
const https = require('https');

const options = {
  host: 'icanhazdadjoke.com',
  path: '/',
  method: 'GET',
  headers: {
    Accept: 'application/json',
  },
  timeout: 2000,
};

const req = https.request(options, (res) => {
  res.setEncoding('utf8');

  let body = '';

  res.on('data', (chunk) => {
    body += chunk;
  });

  res.on('end', () => console.log(body));
});

req.on('error', (err) => {
  if (err.code == 'ECONNRESET') {
    console.log('timeout!');
    return;
  }

  console.error(err);
});

req.on('timeout', () => {
  req.destroy();
});

req.end();
Copied!

The options object supports a timeout property that you can set to timeout a request after a specified period has elapsed (two seconds in this case). You also need to listen for a timeout event on the request and destroy the request manually in its callback function. When a request is destroyed, an ECONNRESET error will be emitted so you must handle it by listening for the error event on the request. You can also emit your own error in destroy():

class TimeoutError extends Error {}
req.destroy(new TimeoutError('Timeout!'));
Copied!

Instead of using the timeout property and timeout event as above, you can also use the setTimeout() method on a request as follows:

const req = https
  .request(options, (res) => {
    res.setEncoding('utf8');

    let body = '';

    res.on('data', (chunk) => {
      body += chunk;
    });

    res.on('end', () => console.log(body));
  })
.setTimeout(2000, () => {
req.destroy();
});
Copied!

Timing out a Fetch API request

The Fetch API was recently merged into Node.js core in Node.js v17.5, so you can start using it in your Node.js applications provided you include the --experimental-fetch argument to the node command.

fetch.js
(async function getDadJoke() {
  try {
    const response = await fetch('https://icanhazdadjoke.com', {
      headers: {
        Accept: 'application/json',
      },
    });
    const json = await response.json();
    console.log(json);
  } catch (err) {
    console.error(err);
  }
})();
Copied!
node fetch.js --experimental-fetch
Copied!

You can omit the --experimental-fetch flag in Node.js v18 or higher:

node fetch.js
Copied!
Output
{
  id: '5wHexPC5hib',
  joke: 'I made a belt out of watches once... It was a waist of time.',
  status: 200
}

In browsers, fetch() usually times out after a set period of time which varies amongst browsers. For example, in Firefox this timeout is set to 90 seconds by default, but in Chromium, it is 300 seconds. In Node.js, no default timeout is set for fetch() requests, but the newly added AbortSignal.timeout() API provides an easy way to cancel a fetch() request when a timeout is reached.

cancel-fetch.js
(async function getDadJoke() {
  try {
    const response = await fetch('https://icanhazdadjoke.com', {
      headers: {
        Accept: 'application/json',
      },
signal: AbortSignal.timeout(3000),
}); const json = await response.json(); console.log(json); } catch (err) { console.error(err); } })();
Copied!

In the above snippet, the AbortSignal.timeout() method cancels the fetch() request if it doesn't resolve within 3 seconds. You can test this out by setting a low timeout value (like 2ms), then execute the script above. You should notice that an AbortError is thrown and caught in the catch block:

Output
AbortError: The operation was aborted
    at abortFetch (node:internal/deps/undici/undici:5623:21)
    at requestObject.signal.addEventListener.once (node:internal/deps/undici/undici:5560:9)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:639:20)
    at AbortSignal.dispatchEvent (node:internal/event_target:581:26)
    at abortSignal (node:internal/abort_controller:291:10)
    at AbortController.abort (node:internal/abort_controller:321:5)
    at AbortSignal.abort (node:internal/deps/undici/undici:4958:36)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:639:20)
    at AbortSignal.dispatchEvent (node:internal/event_target:581:26)
    at abortSignal (node:internal/abort_controller:291:10) {
  code: 'ABORT_ERR'
}

If you're using fetch() extensively in your code, you may want to create a utility function that sets a default timeout on all fetch requests, but that can be easily overridden if necessary.

fetch-with-timeout.js
async function fetchWithTimeout(resource, options = {}) {
  const { timeoutMS = 3000 } = options;

  const response = await fetch(resource, {
    ...options,
    signal: AbortSignal.timeout(timeoutMS),
  });

  return response;
}

. . .
Copied!

The fetchWithTimeout() function above defines a default timeout of 3 seconds on all fetch() requests created through it, but this can be easily overridden by specifying the timeoutMS property in the options object. With this function in place, the getDadJoke() function now looks like this assuming the default timeout is used:

fetch-with-timeout.js
. . .

(async function getDadJoke() {
  try {
    const response = await fetchWithTimeout('https://icanhazdadjoke.com', {
      headers: {
        Accept: 'application/json',
      },
    });

    const json = await response.json();
    console.log(json);
  } catch (err) {
    console.error(err);
  }
})();
Copied!

Now that we have looked at how to set timeouts on the native HTTP request APIs in Node.js, let's consider how to do the same when utilizing some of the most popular third-party HTTP request libraries in the Node.js ecosystem.

Setting timeouts in Axios

The Axios package has a default timeout of 0 which means no timeout, but you can easily change this value by setting a new default:

const axios = require('axios');
axios.defaults.timeout = 5000;
Copied!

With the above in place, all HTTP requests created by axios will wait up to 5 seconds before timing out. You can also override the default value per request in the config object as shown below:

axios.js
(async function getPosts() {
  try {
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/posts',
      {
        headers: {
          Accept: 'application/json',
        },
        timeout: 2000,
      }
    );

    console.log(response.data);
  } catch (err) {
    console.error(err);
  }
})();
Copied!

If you get a timeout error, it will register as ECONNABORTED in the catch block.

Setting timeouts in Got

Got is another popular Node.js package for making HTTP requests, but it also does not have a default timeout so you must set one for yourself on each request:

got.js
const got = require('got');

(async function getPosts() {
  try {
    const data = await got('https://jsonplaceholder.typicode.com/posts', {
      headers: {
        Accept: 'application/json',
      },
      timeout: {
        request: 2000,
      },
    }).json();

    console.log(data);
  } catch (err) {
    console.error(err);
  }
})();
Copied!

Ensure to check out the relevant docs for more information on timeouts in Got.

Adding timeouts to promises

Promises are the recommended way to perform asynchronous operations in Node.js, but there is currently no API to cancel one if it is not fulfilled within a period of time. This is usually not a problem since most async operations will finish within a reasonable time, but it means that a pending promise can potentially take a long time to resolve causing the underlying operation to slow down or hang indefinitely.

Here's an example that simulates a Promise that takes 10 seconds to resolve:

slow-ops.js
const timersPromises = require('timers/promises');

function slowOperation() {
  // resolve in 10 seconds
  return timersPromises.setTimeout(10000);
}

(async function doSomethingAsync() {
  try {
    await slowOperation();
    console.log('Completed slow operation');
  } catch (err) {
    console.error('Failed to complete slow operation due to error:', err);
  }
})();
Copied!

In this example doSomethingAsync() will also take at least 10 seconds to resolve since slowOperation() blocks for 10 seconds. If slowOperation() hangs forever, doSomethingAsync() will also hang forever, and this is often undesirable for a high performance server. The good news is we can control the maximum time that we're prepared to wait for slowOperation() to complete by racing it with another promise that is resolved after a fixed amount of time.

The Promise.race() method receives an iterable object (usually as an Array) that contains one or more promises, and it returns a promise that resolves to the result of the first promise that is fulfilled, while the other promises in the iterable are ignored. This means that the promise returned by Promise.race() is settled with the same value as the first promise that settles amongst the ones in the iterable.

This feature can help you implement Promise timeouts without utilizing any third-party libraries.

promise-with-timeout.js
const timersPromises = require('timers/promises');

function slowOperation() {
  // resolve in 10 seconds
  return timersPromises.setTimeout(10000);
}

function promiseWithTimeout(promiseArg, timeoutMS) {
const timeoutPromise = new Promise((resolve, reject) =>
setTimeout(() => reject(`Timed out after ${timeoutMS} ms.`), timeoutMS)
);
return Promise.race([promiseArg, timeoutPromise]);
}
(async function doSomethingAsync() { try {
await promiseWithTimeout(slowOperation(), 2000);
console.log('Completed slow operation in 10 seconds'); } catch (err) { console.error('Failed to complete slow operation due to error:', err); } })();
Copied!

The promiseWithTimeout() function takes a Promise as its first argument and a millisecond value as its second argument. It creates a new Promise that always rejects after the specified amount of time has elapsed, and races it with the promiseArg, returning the pending Promise from Promise.race() to the caller.

This means that if promiseArg takes more than the specified amount of time (timeoutMS) to be fulfilled, timeoutPromise will reject and promiseWithTimeout() will also reject with the value specified in timeoutPromise.

We can see this in action in doSomethingAsync(). We've decided that slowOperation() should be given a maximum of two seconds to complete. Since slowOperation() always takes 10 seconds, it will miss the deadline so promiseWithTimeout() will reject after 2 seconds and an error will be logged to the console.

Output
Failed to complete slow operation due to error: Timed out after 2000 ms.

If you want to differentiate timeout errors from other types of errors (recommended), you can create a TimeoutError class that extends the Error class and reject with a new instance of TimeoutError as shown below:

timeout-error.js
const timersPromises = require('timers/promises');

function slowOperation() {
  // resolve in 10 seconds
  return timersPromises.setTimeout(10000);
}

class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
function promiseWithTimeout(promiseArg, timeoutMS) { const timeoutPromise = new Promise((resolve, reject) => setTimeout( () => reject(new TimeoutError(`Timed out after ${timeoutMS} ms.`)), timeoutMS ) ); return Promise.race([promiseArg, timeoutPromise]); } (async function doSomethingAsync() { try { await promiseWithTimeout(slowOperation(), 2000); console.log('Completed slow operation'); } catch (err) {
if (err instanceof TimeoutError) {
console.error('Slow operation timed out');
return;
}
console.error('Failed to complete slow operation due to error:', err); } })();
Copied!

Running the script above should now give you a "Slow operation timed out" message:

Output
Slow operation timed out

You will notice that the script above remains active until the 10-second duration of slowOperation() has elapsed despite timing out after 2 seconds. This is because the timersPromises.setTimeout() method used in slowOperation() requires that the Node.js event loop remains active until the scheduled time has elapsed. This is a waste of resources because the result has already been discarded, so we need a way to ensure that scheduled Timeout is also cancelled.

You can use the AbortController class to cancel the promisified setTimer() method as shown below:

abort-controller.js
. . .

function slowOperation() {
const ac = new AbortController();
return {
exec: () => timersPromises.setTimeout(10000, null, { signal: ac.signal }),
cancel: () => ac.abort(),
};
}
. . . (async function doSomethingAsync() {
const slowOps = slowOperation();
try {
await promiseWithTimeout(slowOps.exec(), 2000);
console.log('Completed slow operation'); } catch (err) { if (err instanceof TimeoutError) {
slowOps.cancel();
console.error('Slow operation timed out'); return; } console.error('Failed to complete slow operation due to error:', err); } })();
Copied!

In slowOperation(), a new instance of AbortController is created and set on the timer so that it can be canceled if necessary. Instead of returning the Promise directly, we're returning an object that contains two functions: one to execute the promise, and the other to cancel the timer.

With these changes in place, doSomethingAsync() is updated so that the object from slowOperation() is stored outside the try..catch block. This makes it possible to access its properties in either block. The cancel() function is executed in the catch block when a TimeoutError is detected to prevent slowOperation() from consuming resources after timing out.

We also need a way to cancel the scheduled Timeout in promiseWithTimeout() so that if the promise is settled before the timeout is reached, additional resources are not being consumed by timeoutPromise.

cancel-timeout.js
. . .

function promiseWithTimeout(promiseArg, timeoutMS) {
let timeout;
const timeoutPromise = new Promise((resolve, reject) => {
timeout = setTimeout(() => {
reject(new TimeoutError(`Timed out after ${timeoutMS} ms.`));
}, timeoutMS);
});
return Promise.race([promiseArg, timeoutPromise]).finally(() =>
clearTimeout(timeout)
);
}
. . .
Copied!

The promiseWithTimeout() option has been updated such that the Timeout value returned by the global setTimeout() function is stored in a timeout variable. This gives the ability to clear the timeout using the clearTimeout() function in the finally() method attached to the return value of Promise.race(). This ensures that the timer is canceled immediately the promise settles.

You can observe the result of this change by modifying the timeout value in slowOperation() to something like 200ms. You'll notice that the script prints a success message and exits immediately. Without canceling the timeout in the finally() method, the script will continue to hang until the two seconds have elapsed despite the fact that promiseArg has already been settled.

If you want to use this promiseWithTimeout() solution in TypeScript, here are the appropriate types to use:

promise-with-timeout.ts
function promiseWithTimeout<T>(
  promiseArg: Promise<T>,
  timeoutMS: number
): Promise<T> {
  let timeout: NodeJS.Timeout;
  const timeoutPromise = new Promise<never>((resolve, reject) => {
    timeout = setTimeout(() => {
      reject(new TimeoutError(`Timed out after ${timeoutMS} ms.`));
    }, timeoutMS);
  });

  return Promise.race([promiseArg, timeoutPromise]).finally(() =>
    clearTimeout(timeout)
  );
}
Copied!

In this snippet, promiseWithTimeout() is defined as a generic function that accepts a generic type parameter T, which is what promiseArg resolves to. The function's return value is also a Promise that resolves to type T. We've also set the return value of timeoutPromise to Promise<never> to reflect that it will always reject.

How to choose a timeout value

So far, we've discussed various ways to set timeout values in Node.js. It is equally important to figure out what the timeout value should be in a given situation depending on the application and the operation that's being performed. A timeout value that is too low will lead to unnecessary errors, but one that is too high may decrease application responsiveness when slowdowns or outages occur, and increase susceptibility to malicious attacks.

Generally speaking, higher timeout values can be used for background or scheduled tasks while immediate tasks should have shorter timeouts. Throughout this post, we used arbitrary timeout values to demonstrate the concepts but that's not a good strategy for a resilient application. Therefore, it is necessary to briefly discuss how you might go about this.

With external API calls, you can start by setting your timeouts to a high value initially, then run a load test to gather some data about the API's throughput, latency, response times, and error rate under load. If you use a tool like Artillery, you can easily gather such data, and also find out the 95th and 99th percentile response times.

For example, if you have a 99th percentile response time of 500ms, it means that 99% of requests to such endpoint was fulfilled in 500ms or less. You can then multiply the 99th percentile value by 3 or 4 to get a baseline timeout for that specific endpoint. With such timeouts in place, you can be reasonably sure that it should suffice for over 99% of requests to the endpoint.

That being said, it's often necessary to refine the timeout value especially if you start getting a high number of timeout errors, so make sure to have a monitoring system in place for tracking such metrics. It may also be necessary to set a timeout that is much greater than the calculated baseline timeout when a critical operation is being performed (like a payment transaction for example).

Conclusion

In this article, we discussed the importance of timeouts in Node.js, and how to set timeouts in a variety of scenarios so that your application remains responsive even when third-party APIs are experiencing slowdowns. We also briefly touched on a simple process for how you might choose a timeout value for an HTTP request, and the importance of monitoring and refining your timeout values.

You can find all the code snippets used throughout this article in this GitHub repository. Thanks for reading, and happy coding!

Centralize all your logs into one place.
Analyze, correlate and filter logs with SQL.
Create actionable
dashboards with Grafana.
Share and comment with built-in collaboration.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.