Skip to Content
Drash v3 is in beta. APIs may change.

RateLimiter

Clients are identified by a header they send themselves. Unless that header is one you issue and verify, treat this as request shaping for cooperative clients — not as an abuse control.

Summary

The RateLimiter middleware counts requests per client within a rolling time window and rejects the ones over the limit. It is added to resources using a resource group.

The highlighted section below shows how it is added:

// Code is shortened for brevity import { ResourceGroup } from "@drashland/drash/modules/http.native.js"; import { RateLimiter } from "@drashland/drash/modules/middleware/RateLimiter.js"; const group = ResourceGroup .builder() .middleware(RateLimiter({ max_requests: 100, // 100 requests ... rate_limit_time_window_length: 60000, // ... per minute client_id_header_name: "x-api-key", // Counted against this header's value })) .resources(Users) .build();

Like the other pre-built middleware, RateLimiter is a factory. Calling it returns a middleware class with your options already bound to it.

Configuring Options

The Defaults

OptionTypeDefaultNotes
max_requestsnumber3Requests allowed per window
rate_limit_time_window_lengthnumber60000Window length in milliseconds
client_id_header_namestring"x-drash-ratelimit-client-id"Request header identifying the client
throw_if_connection_header_name_missingbooleantrueThrow when that header is absent

Options you leave out keep their defaults, so each example below sets only what it is demonstrating.

Tuning the Window

max_requests and rate_limit_time_window_length are read together, and the second is in milliseconds:

// 100 requests per minute. RateLimiter({ max_requests: 100, rate_limit_time_window_length: 60000 }); // 10 requests per second, for a burst-sensitive endpoint. RateLimiter({ max_requests: 10, rate_limit_time_window_length: 1000 }); // The defaults: 3 requests per minute. RateLimiter();

Identifying Clients

client_id_header_name names the request header the limiter counts against. The default is x-drash-ratelimit-client-id, which nothing sets for you — point it at a header your clients or your proxy actually send:

// Behind a proxy that forwards the caller's address. RateLimiter({ client_id_header_name: "x-forwarded-for", });

There is no IP fallback. A client is whatever that header says it is, so anyone can present a fresh value and get a fresh budget.

Handling a Missing Header

throw_if_connection_header_name_missing decides what happens when a request arrives without that header. true rejects it with a 400; false lets it straight through, since there is no client to count:

// Fail open. A request with no identifying header is not counted at all. RateLimiter({ throw_if_connection_header_name_missing: false, });

Setting this to false makes the limit trivially avoidable — omit the header and you are never counted. Prefer true unless something upstream guarantees the header is present.

How It Works

TLDR

The middleware reads the client’s header, counts the request against that client’s budget, and either passes the request to your resource or throws a 429. Either way the response comes back tagged with X-RateLimit-* headers.

Detailed Explanation

Unlike ETag, this middleware does its work before your resource. A request over the limit never reaches the resource at all, which is the point — the work is what you are trying to avoid.

Exceeding the limit throws an HTTPError with status 429 Too Many Requests. Responses carry:

HeaderMeaning
X-RateLimit-Limitmax_requests
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetWhen the window resets
X-Retry-AfterSent with 429 responses

The retry header is X-Retry-After, not the standard Retry-After. Clients and proxies that back off automatically on Retry-After will not see this one.

State and Scope

Counters live in memory, per middleware instance. Because resource groups construct middleware fresh per resource, two resources in the same group each get their own independent counters rather than sharing one budget.

In-memory state also means limits are per process. Across several instances or workers each keeps its own counts, so a shared store would be needed for a global limit.

Data Flow

As you can see above, a rejected request stops at the middleware and your resource is never called.

Extending This Middleware

The module exports the class as well as the factory, so you can extend it:

import { RateLimiterMiddleware } from "@drashland/drash/modules/middleware/RateLimiter.js"; class LoggedRateLimiter extends RateLimiterMiddleware { constructor() { // Your options go here, the same ones you would pass to RateLimiter(). super({ max_requests: 100 }); } public override ALL(request: Request) { console.log(`Client: ${request.headers.get("x-api-key")}`); // Hand it back to the original middleware and get out of the way. return super.ALL(request); } }
ExportWhat It Is
RateLimiterThe factory. Returns a configured middleware class
RateLimiterMiddlewareThe middleware class itself, for extending
defaultOptionsThe defaults listed above
OptionsThe options type

RateLimiter.ts is the middleware. The three classes it is built from live in modules/middleware/rate_limiter/. You reach for these only when handling a rate-limit response yourself.

RateLimitedClient

import { RateLimitedClient } from "@drashland/drash/modules/middleware/rate_limiter/RateLimitedClient.js";

Per-client request accounting — one instance tracks one client’s activity inside the current window. Every member is a getter; the class exposes no public setters.

GetterNotes
num_requests_madeRequests made in the window. Starts at 0.
requests_remainingWhat is left before the limit is hit.
max_requests_allowed_in_time_windowThe configured ceiling.
hit_request_limittrue once num_requests_made exceeds the ceiling.
current_request_timeTimestamp of the request being handled. -1 before any.
rate_limit_window_end_timeWhen the current window closes.
rate_limit_window_time_elapsedHow far into the window this client is.

RateLimiterErrorResponse

import { RateLimiterErrorResponse } from "@drashland/drash/modules/middleware/rate_limiter/RateLimiterErrorResponse.js"; constructor(status: ResponseStatus, response: Response)

Extends HTTPError, carrying the Response the middleware already built alongside the status.

This is the one worth knowing about in a .catch() block. Because it is an HTTPError, an error.name === "HTTPError" check catches it — and unlike a bare HTTPError, it hands you a ready-made response with the X-RateLimit-* headers already set.

RateLimitResponse

import { RateLimitResponseBuilder } from "@drashland/drash/modules/middleware/rate_limiter/RateLimitResponse.js";

RateLimitResponseBuilder extends ResponseBuilder, taking an existing Response in its constructor and adding the rate-limit headers to it. The module also exports a response(response) factory.

MethodNotes
addRateLimitHeaders(values)Sets Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.
build()Overridden. Values set on the builder win; anything unset falls back to the wrapped response.
Last updated on