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

@meistrari/serve

v1.1.1

Published

Start applications with a batteries-included graceful shutdown

Downloads

1,617

Readme

@meistrari/serve

Starts applications with the lifecycle handling every service ends up rewriting: a server with sane defaults, a graceful shutdown that drains in-flight requests, and crash logging that actually reaches Datadog.

Features

  • 🚀 Bun.serve with the defaults our services want, on a dedicated /bun export
  • 🧹 Graceful shutdown on SIGINT/SIGTERM, draining in-flight requests before exiting
  • 🔌 Ordered cleanup hooks for database pools, queue consumers, and analytics clients
  • 💥 uncaughtException/unhandledRejection logged as structured FATAL instead of a truncated stack
  • 📋 Logs through @meistrari/logger
  • 🎯 TypeScript support

Installation

bun add @meistrari/serve

Usage

Bun

import { serve } from '@meistrari/serve/bun'
import { app } from './app'

serve({
    fetch: app.fetch,
})

That's the whole entrypoint. It listens on PORT, logs a startup line, and on SIGTERM stops accepting connections, waits for in-flight requests to finish, and exits 0.

Every Bun.serve option is accepted and takes precedence over the defaults, and the return value is the plain Bun.Server:

const server = serve({
    fetch: app.fetch,
    port: 8080,
    reusePort: true,
})

Logging through your own logger

The startup line and the shutdown reporting go through a fresh @meistrari/logger instance by default. Applications that build their own — a level from the environment, extra bindings — should pass it, so the lifecycle lines carry the same fields as every other log the service emits:

import { logger } from './logger'

serve({
    fetch: app.fetch,
    logger,
})

Closing other resources

Hooks run in order, after the HTTP server stops accepting connections — so requests still in flight keep working while they finish:

import { serve } from '@meistrari/serve/bun'

serve({
    fetch: app.fetch,
    onShutdown: [closeDbPool, closeKvClient],
})

Anything created later can register itself with onShutdown, which returns a function that unregisters the hook:

import { onShutdown } from '@meistrari/serve'

const consumer = await startConsumer()

const unregister = onShutdown(() => consumer.stop(), 'stopConsumer')

Register after the server, not before

Hooks run in registration order, and the hook that drains the HTTP server is registered by serve — so a hook registered before serve runs before the drain, closing the resource under the requests that still need it.

That's easy to do by accident, because it doesn't take an explicit ordering mistake. A module body runs before the body of the module that imported it, so a top-level onShutdown in a db.ts that the entrypoint imports is always registered first:

// db.ts — registered at import time, so it runs BEFORE the server drains
export const pool = new Pool()

onShutdown(() => pool.end(), 'closeDbPool')

Pass those to serve instead, which registers them after its own drain hook:

import { serve } from '@meistrari/serve/bun'
import { closeDbPool } from './db'

serve({
    fetch: app.fetch,
    onShutdown: [closeDbPool],
})

Keep onShutdown for what is created after the server is already listening, and order the list itself outside in: whatever a later hook depends on has to still be alive when it runs.

A hook that throws doesn't abort the shutdown: the error is logged and the remaining hooks still run. The exit code stays 0 — the process was asked to stop and it stopped, so a failed cleanup shouldn't make the orchestrator report a crashed container.

Outside Bun

The root export is runtime-agnostic, for workers, consumers, and anything else without an HTTP server:

import { setupGracefulShutdown } from '@meistrari/serve'

setupGracefulShutdown({
    hooks: [stopConsumer, closeDbPool],
})

serve, setupGracefulShutdown and onShutdown all share one controller per process, which is what an application wants: calling them in any order reconfigures the existing one instead of installing a second set of process listeners, and hooks registered earlier are kept.

Testing a shutdown

Tests want the opposite of a shared controller. createShutdownController builds one that touches no global state, and exit replaces process.exit so the shutdown can be asserted without taking the test runner down with it:

import { createShutdownController } from '@meistrari/serve'

const exitCodes: number[] = []
const controller = createShutdownController({
    exit: code => exitCodes.push(code),
})

controller.onShutdown(closeDbPool)
await controller.shutdown('test')

expect(exitCodes).toEqual([0])

// Removes every process listener this controller installed
controller.dispose()

Shutting down on your own terms

import { isShuttingDown, shutdown } from '@meistrari/serve'

if (await healthCheckFailed()) {
    await shutdown('health-check-failed')
}

// Useful to stop pulling new work while cleanup is running
async function pollQueue() {
    while (!isShuttingDown()) {
        await handleNextMessage()
    }
}

Environment variables

| Variable | Default | Description | | --- | --- | --- | | PORT | 3000 | Port the Bun server listens on | | ENVIRONMENT | unknown | development enables Bun.serve's development mode |

@meistrari/logger also reads SERVICE_NAME, SERVICE_VERSION and ENVIRONMENT.

Defaults

idleTimeout: 255

Bun closes idle keep-alive connections after idleTimeout seconds (10 by default). kgateway/Envoy pools and reuses upstream connections for far longer, so at 10s Bun kept closing connections Envoy still believed were alive; when Envoy dispatched onto one mid-close it surfaced as a 503 with response flag UC (upstream connection termination), which the gateway retry policy does not retry. Holding connections for the maximum Bun allows (255s) makes Envoy always the side that closes first.

No shutdown timeout

Kubernetes, Docker and systemd already bound the shutdown with their own grace period before sending SIGKILL. A second timeout here would only race it, usually cutting cleanup short earlier than intended. Set timeout if nothing else supervises the process:

serve({
    fetch: app.fetch,
    gracefulShutdown: { timeout: 10_000 },
})

About SIGKILL

SIGKILL and SIGSTOP can't be handled — the kernel terminates the process without ever delivering them, so no cleanup is possible by design. Orchestrators send SIGTERM first and only escalate to SIGKILL after the grace period, which is the window this package uses. Make sure your terminationGracePeriodSeconds is longer than your slowest request.

Uncaught errors

uncaughtException and unhandledRejection are logged as FATAL with the full stack, the shutdown hooks run, and the process exits 1. Bun's default handler prints a truncated single-line stack that rarely survives log ingestion, and it lands in the container runtime's stderr rather than the structured logs.

The two aren't treated the same, because they aren't the same kind of failure:

  • unhandledRejection kills one async chain and leaves the rest of the process alone — usually a missing .catch() on a fire-and-forget call. It drains exactly like a SIGTERM does, bounded by timeout.
  • uncaughtException unwinds the stack from an arbitrary point, so no hook can be trusted to finish. Draining is still attempted — every in-flight request that isn't the one that threw gets a real response instead of a connection reset — but under a uncaughtExceptionTimeout deadline (5s) that always wins.

That deadline matters more than it looks: closeServer waits for in-flight requests, and the request whose handler threw will never produce a response. The hook it's waiting on can hang forever, and a hung process is worse than a crashed one — it holds its listening socket and stays in the load balancer's endpoint list until SIGKILL lands.

Set uncaughtExceptionTimeout: false to skip the hooks and exit immediately instead.

A fatal error arriving during a shutdown that's already running doesn't restart or abort it — that would run the hooks twice or drop the requests they exist to protect. The shutdown finishes, capped by the deadline, and the exit code becomes 1.

Options

Everything below gracefulShutdown is also accepted by setupGracefulShutdown.

| Option | Default | Description | | --- | --- | --- | | logger | default @meistrari/logger | Logger for the startup line and shutdown reporting | | onShutdown | [] | Cleanup functions, run in order after the server drains (hooks on setupGracefulShutdown) | | gracefulShutdown | true | false opts out entirely; an object overrides the settings below | | signals | ['SIGINT', 'SIGTERM'] | Signals that trigger the shutdown | | handleUncaughtErrors | true | Log uncaught errors as FATAL, run the hooks, and exit 1 | | uncaughtExceptionTimeout | 5000 | Milliseconds the hooks get after an uncaughtException; false skips them | | handleDisconnect | true | Shut down when a node:cluster primary disconnects the worker | | timeout | none | Milliseconds before exiting regardless of the hooks | | forceExitOnSecondSignal | true | Exit immediately on a second Ctrl+C |