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

async-bulkhead-express

v0.2.0

Published

Express middleware for route-level bulkheads and fail-fast overload protection.

Downloads

269

Readme

async-bulkhead-express

Express middleware for fail-fast admission control on overloaded routes.

async-bulkhead-express wraps a bulkhead primitive in Express-native middleware so expensive or slow routes can reject overload early instead of quietly dragging down the rest of the app.

What this package is

  • Route-level and router-level admission control for Express
  • Local overload isolation with bounded active work
  • Shared-capacity protection for related routes
  • Fail-fast rejection when protected capacity is full
  • Optional bounded queueing with queue wait timeouts
  • Express-friendly hooks and custom overload responses

What this package is not

  • A rate limiter
  • A retry library
  • A circuit breaker
  • A downstream request timeout library
  • A distributed quota or cross-instance coordination system

Why 503 instead of 429?

This package models service overload, not client abuse or per-user quota exhaustion. When a protected route is full, the default response is 503 Service Unavailable with a small JSON body.

{ "error": "service_unavailable", "reason": "bulkhead_rejected" }

Install

npm i async-bulkhead-express express

Quick start

Protect a single route

import express from 'express';
import { createBulkheadMiddleware } from 'async-bulkhead-express';

const app = express();

const searchBulkhead = createBulkheadMiddleware({
  name: 'search',
  maxConcurrent: 20,
  maxQueue: 0,
});

app.get('/search', searchBulkhead, async (_req, res) => {
  const results = await search();
  res.json(results);
});

Share one capacity pool across multiple routes

Use a reusable instance when related routes should contend for the same protected capacity.

import express from 'express';
import { createExpressBulkhead } from 'async-bulkhead-express';

const app = express();

const payments = createExpressBulkhead({
  name: 'payments',
  maxConcurrent: 10,
  maxQueue: 0,
});

app.post('/charge', payments.middleware(), chargeHandler);
app.post('/refund', payments.middleware(), refundHandler);

Use bounded queueing

maxQueue allows a small number of requests to wait for capacity. queueWaitTimeoutMs bounds how long a queued request can wait.

const reports = createExpressBulkhead({
  name: 'reports',
  maxConcurrent: 4,
  maxQueue: 8,
  queueWaitTimeoutMs: 250,
});

Queued acquisition is aborted by default if the client disconnects before admission. Set abortOnClientClose: false only if you have a specific reason to keep queued acquisition alive after disconnect.

Customize overload responses

const search = createExpressBulkhead({
  name: 'search',
  maxConcurrent: 20,
  maxQueue: 0,
  rejectResponse({ res, reason }) {
    res
      .status(503)
      .set('Retry-After', '1')
      .json({ code: 'BUSY', reason });
  },
});

If rejectResponse does not send a response, the default 503 JSON response is used.

Skip selected requests

skip(req) lets you apply a bulkhead at router level while bypassing cheap routes such as health checks or CORS preflight.

const apiBulkhead = createExpressBulkhead({
  name: 'api',
  maxConcurrent: 50,
  maxQueue: 0,
  skip: (req) => req.path === '/healthz' || req.method === 'OPTIONS',
});

app.use('/api', apiBulkhead.middleware(), apiRouter);

Add observability labels and metadata

Use routeLabel to keep metrics low-cardinality and metadata(req) to attach request-scoped details to hook events.

const users = createExpressBulkhead({
  name: 'users',
  maxConcurrent: 15,
  maxQueue: 5,
  routeLabel: 'GET /users/:id',
  metadata: (req) => ({ requestId: req.header('x-request-id') }),
  onReject(event) {
    metrics.increment('bulkhead.reject', {
      bulkhead: event.name,
      route: event.route,
      reason: event.reason,
    });
  },
});

API

createBulkheadMiddleware(options)

Creates a single Express middleware function backed by an internal bulkhead instance.

createExpressBulkhead(options)

Creates a reusable bulkhead wrapper with:

  • middleware(): RequestHandler
  • stats(): ExpressBulkheadStats
  • close(): void
  • drain(): Promise

Use this form when multiple routes should share one capacity pool or when the app needs explicit shutdown/drain behavior.

ExpressBulkheadOptions

interface ExpressBulkheadOptions {
  name?: string;
  maxConcurrent: number;
  maxQueue?: number;
  queueWaitTimeoutMs?: number;
  abortOnClientClose?: boolean;
  skip?: (req: Request) => boolean;
  pathMode?: 'path' | 'originalUrl' | 'route';
  routeLabel?: string | ((req: Request) => string | undefined);
  metadata?: (req: Request) => Record<string, unknown> | undefined;
  rejectResponse?: (context: ExpressBulkheadRejectResponseContext) => void | Promise<void>;
  onAdmit?: (event: ExpressBulkheadAdmitEvent) => void;
  onReject?: (event: ExpressBulkheadRejectEvent) => void;
  onRelease?: (event: ExpressBulkheadReleaseEvent) => void;
}

ExpressBulkheadStats

interface ExpressBulkheadStats {
  name?: string;
  inFlight: number;
  pending: number;
  maxConcurrent: number;
  maxQueue: number;
  closed: boolean;
}

Reject reasons

type ExpressBulkheadRejectReason =
  | 'bulkhead_rejected'
  | 'bulkhead_closed'
  | 'queue_timeout'
  | 'request_aborted';

Lifecycle behavior

  • Admission happens before downstream route work begins.
  • Rejected requests do not call downstream handlers.
  • maxQueue: 0 gives fail-fast behavior with no queueing.
  • maxQueue > 0 enables bounded waiting.
  • queueWaitTimeoutMs applies only while waiting for admission.
  • Queued acquisition is aborted on client disconnect by default.
  • Admitted requests hold capacity until the HTTP lifecycle ends.
  • Capacity is released on finish or close, whichever happens first.
  • Duplicate lifecycle events do not double-release capacity.

Hooks

Hooks are synchronous fire-and-forget observability callbacks:

  • onAdmit
  • onReject
  • onRelease

Hook exceptions are swallowed so observability code does not break request flow.

Development

npm test
npm run verify