npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dineroregnskab/node-service

v1.1.0

Published

Keep-alive and graceful shutdown for Node HTTP services behind a load balancer

Readme

node-service

How a Node HTTP service behind a load balancer or proxy should serve and stop:

  • Keep-alive: idle connections stay open longer than the proxy keeps its own, so the proxy is always the side that closes them.
  • Graceful shutdown: on SIGTERM, fail readiness first, keep serving for a while, and only then stop listening.

Why the keep-alive matters

A proxy in front of the server pools its connections and sends the next request down an idle one. Node closes an idle connection after 5 seconds by default, much sooner than proxies do (Envoy's default is an hour). When the server closes a connection at the moment the proxy reuses it, that request fails and the caller gets an error from the proxy, typically a 502 or 503, for a request the service never saw.

gracefulShutdown({ server }) sets server.keepAliveTimeout from KeepAlive__TimeoutSeconds, or to 61 minutes (KEEP_ALIVE_TIMEOUT_MS) when that is unset. The proxy then closes idle connections before the server would, and that race goes away. It costs nothing at shutdown: since Node 19, server.close() drops idle connections straight away. Set the variable from the deployment, a little above the idle timeout of the proxy in front.

Why the order matters

A service that stops listening the moment it takes SIGTERM is still in the load balancer's rotation. A health check runs on an interval and its verdict still has to propagate, so for a few seconds afterwards the load balancer keeps sending requests to a socket nobody is accepting — and the caller sees a connection error rather than a clean response.

Failing the readiness check immediately and then waiting is what closes that window: the load balancer takes the instance out while it is still able to serve.

Install

npm install @dineroregnskab/node-service

Use

const { gracefulShutdown } = require('@dineroregnskab/node-service');

const app = require('http').createServer(handler);

const shutdown = gracefulShutdown({ server: app });

function handler(req, res) {
  if (req.url === '/health/ready') {
    res.writeHead(shutdown.readyStatus());
    return res.end('Healthy!');
  }
  // ...
}

app.listen(3000);

readyStatus() returns 200 while serving and 425 once SIGTERM has arrived. A service with readiness conditions of its own passes them in, and any false wins:

res.writeHead(shutdown.readyStatus(!restarting));

isShuttingDown() is the same state as a predicate, for code that is not a readiness endpoint — suppressing a restart-on-fatal path during shutdown, for instance.

Teardown beyond the HTTP server goes in onClosing, which runs once the server has stopped accepting connections, while it is still closing:

gracefulShutdown({
  server: app,
  onClosing: () => new Promise((resolve) => io.close(resolve)),
});

That ordering is deliberate. server.close() stops new connections straight away, but its callback waits for the open ones — and a long-lived connection such as a WebSocket is exactly what the teardown is there to end. Running the teardown only after that callback would wait for something only the teardown can cause, until the timeout gave up and killed the process with the clients still attached.

Configuration

| Variable | Default | What it is | | --- | --- | --- | | Shutdown__DelaySeconds | 10 | How long to keep serving after SIGTERM while readiness fails, so the load balancer can take the instance out of rotation. | | Shutdown__TimeoutSeconds | 30 | Ceiling on closing the server afterwards. Past it the process exits regardless. | | KeepAlive__TimeoutSeconds | 3660 | How long an idle connection stays open. Keep it above the idle timeout of the proxy in front. |

Set them from the deployment rather than in code, so the delay can track the health check interval of whatever sits in front of the service, and the keep-alive its idle timeout. The defaults are what a service gets when nothing sets them: a developer running it by hand, or a test.

An unset or empty value falls back to the default. So does one that is not a non-negative number, which also logs a warning.

The timeout is not decoration

server.close() stops accepting new connections but waits for in-flight ones, and a long-lived connection — a websocket — can keep it waiting indefinitely. Without the ceiling the process would sit there until whatever supervises it loses patience and kills it, which is the behaviour this package exists to avoid.

API

| Export | What it does | | --- | --- | | gracefulShutdown(options?) | Raises the server's keep-alive timeout, installs the SIGTERM handler, returns { isShuttingDown, readyStatus }. | | readSeconds(name, fallback) | Reads a non-negative number of seconds from the environment. | | DEFAULT_DELAY_SECONDS / DEFAULT_TIMEOUT_SECONDS | 10 and 30. | | KEEP_ALIVE_TIMEOUT_MS | 3660000, 61 minutes: the keep-alive when KeepAlive__TimeoutSeconds is unset. | | READY / NOT_READY | 200 and 425. |

options: server (keep-alive raised, closed after the delay), onClosing (teardown while it closes, inside the timeout), plus exit and process for tests.

Test

npm test