The outbound integrations protect you from your dependencies. The inbound middleware protects your service from its callers. One middleware gives every request:
a deadline that propagates to every pipeline called while handling it, even ones that never receive a signal;
load shedding: a cap on requests in flight, with a short queue, and a fast 503 beyond it;
a rate limit, global or per client, with RateLimit-* headers and 429 + Retry-After beyond it;
cancellation when the client disconnects;
JSON error responses with the right status for every resilience error raised in your handlers.
Both live in @ylcnfrht/resilience/http and import no framework. Fastify and Koa have their own pages.
The rate limit is checked first, so a client over its quota never takes a concurrency slot. The timeout starts once the request has a slot: time spent in the queue is bounded by queueTimeout, not by timeout.
Deadline propagation. Your handlers run inside the middleware’s execution (through AsyncLocalStorage). Any pipeline executed underneath, such as a resilient fetch, a database pipeline or a NestJS service, links its signal to the request’s. When the budget runs out they are all aborted. getResilienceSignal() returns the request’s signal anywhere in the call.
Timeout response. When timeout passes and nothing was sent yet, the client gets 504. Your handler keeps running until its own awaits notice the aborted signal, so check res.headersSent before writing late.
Client disconnect. When the connection closes before the response finished, the signal aborts with Client disconnected and nothing is sent.
When the slot is freed. Express holds the concurrency slot until the response finishes or the connection closes. Hono holds it until the handler chain returns.
Other errors. Errors that aren’t resilience errors are left to the framework: Express gets them through next(error) as usual, Hono’s middleware rethrows them.
The response body is always JSON:
{ "statusCode": 429, "error": "RateLimiterRejectedError", "message": "The operation could not be executed due to rate limiting. Retry after 2400ms." }
with a Retry-After header (seconds, rounded up) when the error knows when to retry: rate limits and open circuits.
The middleware only answers for its own rejections. A resilience error raised inside your handler (an open circuit or a timeout of a downstream call) goes to the framework’s error handling like any other error. One exception: when the request’s own timeout fires, nested pipelines rethrow that same TimeoutRejectedError, and Hono (like Fastify and Koa) still answers 504 for it. In Express the handler runs outside the middleware, so that error reaches your error handler; mapping it as below gives the same 504. Map it there:
describeResilienceError(error) and httpStatusFor(error) map an error by its code, not by instanceof, so they work across duplicate copies of the package.
code
Error
HTTP
Why
TIMEOUT
TimeoutRejectedError
504
The work didn’t finish in its budget.
RATE_LIMITED
RateLimiterRejectedError
429
The caller is over its rate.
PERMIT_LIMIT_EXCEEDED
PermitLimitExceededError
429
One execution asked for more permits than the limit holds.
describeResilienceError() returns { code, httpStatus, retryAfter, message, source } (retryAfter in seconds) and doesn’t depend on HTTP. Use it wherever you build an error for a client:
// Apollo Server / GraphQL Yoga: formatError, or a resolver wrapper.
Transport-neutral description of a resilience error, or undefined for any other error. Use it to build an HTTP
response, GraphQL extensions: { code, httpStatus, retryAfter }, a gRPC status, or a WebSocket error frame.
Works across duplicate copies of this package (uses error codes, not instanceof).
Pass pipeline to use any strategies: queue priorities, the adaptive concurrency limiter instead of a fixed limit, a Redis-backed rate limit. The inbound request is in context.properties under HTTP_REQUEST, for partition keys and priorities:
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
context.properties key of the inbound request (Express req / Hono c), for your own partitioned limiters.
HTTP_REQUEST } from'@ylcnfrht/resilience/http';
type
typeReq= {
headers:Record<string, string|undefined>;
}
Req= {
headers: Record<string, string |undefined>
headers:
typeRecord<Kextendskeyofany, T> = { [PinK]:T; }
Construct a type with a set of properties K of type T
Record<string, string|undefined> };
const
constplan: (ctx: {
properties:Map<string, unknown>;
}) =>string|undefined
plan= (
ctx: {
properties: Map<string, unknown>;
}
ctx: {
properties: Map<string, unknown>
properties:
interfaceMap<K, V>
Map<string, unknown> }) => (
ctx: {
properties: Map<string, unknown>;
}
ctx.
properties: Map<string, unknown>
properties.
Map<string, unknown>.get(key: string): unknown
Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().