Skip to content
Buy me a coffee

Fastify

fastifyResilience gives every request the same inbound protection as the Express and Hono middleware: a deadline that propagates to every pipeline called underneath, load shedding, a rate limit with RateLimit-* headers, cancellation on client disconnect, and JSON 504/503/429/409/403 responses.

Terminal window
npm i @ylcnfrht/resilience fastify # fastify >= 4 (optional peer dependency)
import Fastify from 'fastify';
import { getResilienceSignal } from '@ylcnfrht/resilience';
import { fastifyResilience } from '@ylcnfrht/resilience/fastify';
const app = Fastify();
// Register and await it BEFORE declaring routes.
await app.register(fastifyResilience, {
timeout: '5s',
concurrency: { permitLimit: 200, queueLimit: 100, queueTimeout: '1s' },
rateLimit: { key: (req) => req.ip, permitLimit: 100, window: '1m' },
});
app.get('/orders/:id', async (req) => {
// Aborted when the 5 s budget runs out or the client disconnects.
return db.orders.find((req.params as { id: string }).id, { signal: getResilienceSignal() });
});
await app.listen({ port: 3000 });

The plugin takes InboundOptions, the same as the Express middleware: timeout, concurrency, rateLimit, rateLimitHeaders, pipeline. See the options table. rateLimit.key receives the Fastify request.

  • onRequest hook. Each request enters the pipeline (rate limit → concurrency → timeout) in onRequest. The slot is held until the raw response finishes or the connection closes.
  • onRoute hook. Fastify reaches your handler through body parsing and other async hooks, which can lose AsyncLocalStorage (any callback-based hook or connection pool does). The plugin wraps every route handler declared after it to re-enter the request’s context, so getResilienceSignal() and nested pipelines still see the request deadline. That is why it must be registered, and awaited, before your routes.
  • Encapsulation. The plugin skips Fastify’s encapsulation, like a fastify-plugin plugin. Its hooks land on the instance you register it on: on the root instance they cover the whole app; inside a plugin they cover that plugin’s routes (and its children).
  • Responses. A rejection is sent with reply.code(status).headers(…).send({ statusCode, error, message }), plus Retry-After when known. Rate-limit headers are added to every response when a rate limiter is configured.
  • Errors. If the pipeline fails with something other than a resilience error before your handler started, it goes to Fastify through the hook’s next(error).

Errors thrown by your handlers go to Fastify’s error handler as usual. To map resilience errors raised downstream (an open circuit, a timeout), use describeResilienceError:

import { describeResilienceError } from '@ylcnfrht/resilience/http';
app.setErrorHandler((error, req, reply) => {
const d = describeResilienceError(error);
if (!d) return reply.send(error); // Fastify's default handling
if (d.retryAfter !== undefined) reply.header('retry-after', String(d.retryAfter));
return reply.code(d.httpStatus).send({ code: d.code, message: d.message });
});