- Enforcing timeouts on client connections.
- Canceling outgoing HTTP requests after a deadline.
- Adding timeouts to Promises.
- A strategy for choosing timeout values.
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:
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
After the project is downloaded, cd
into the nodejs-timeouts
directory and
run the command below to download all the necessary dependencies:
npm install
Save hours of sifting through Node.js logs. Centralize with Better Stack and start visualizing your log data in minutes.
See the Node.js demo dashboard live.
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:
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);
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
curl http://localhost:3000
You should see the following output after 5 seconds, indicating that a response was not received from the server due to a closed connection.
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();
});
. . .
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();
});
This method of setting server timeouts also works with Express servers:
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();
});
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);
});
This will cause requests to the site root to timeout after 20 seconds of inactivity instead of the 5 second default.
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.
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.
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();
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!'));
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();
});
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.
(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);
}
})();
node fetch.js --experimental-fetch
You can omit the --experimental-fetch
flag in Node.js v18 or higher:
node fetch.js
{
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.
(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);
}
})();
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 2
ms), then execute the script above. You should
notice that an AbortError
is thrown and caught in the catch
block:
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.
async function fetchWithTimeout(resource, options = {}) {
const { timeoutMS = 3000 } = options;
const response = await fetch(resource, {
...options,
signal: AbortSignal.timeout(timeoutMS),
});
return response;
}
. . .
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:
. . .
(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);
}
})();
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.
Head over to Better Uptime and start monitoring your endpoints in 2 minutes
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;
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:
(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);
}
})();
If you get a timeout error, it will register as ECONNABORTED
in the catch
block.
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:
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);
}
})();
Ensure to check out the relevant docs for more information on timeouts in Got.
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:
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);
}
})();
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.
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);
}
})();
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.
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:
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);
}
})();
Running the script above should now give you a "Slow operation timed out" message:
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:
. . .
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);
}
})();
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
.
. . .
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)
);
}
. . .
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:
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)
);
}
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.
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).
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!
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for usWrite a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github