AbsoluteJS

Rate Limit

A first-class, from-scratch, 2026 rate limit for Bun + Elysia. GCRA by default — the algorithm Stripe uses — with token bucket and sliding-window also bundled. IETF draft-09 RateLimit-* headers, IPv6 grouping by /64, X-Forwarded-For trust modes, BigInt nanosecond TAT, and a lazy-TTL LRU memory store with no background sweeper.

#Choose the traffic behavior you mean

The algorithms share a store contract but intentionally expose different traffic semantics.

Best for: A precise general-purpose limit with smooth behavior and constant storage.

Tradeoffs: The model is less intuitive to explain than a visible request counter.

Requirements
  • One theoretical-arrival-time value per key

#How proxy-aware client identity is derived

Only explicitly trusted network hops may influence the client identity used as a quota key.

  1. Observe
    Receive the socket peer address and any forwarded-address headers.
  2. Bound trust
    Walk back only the configured number of trusted proxy hops.
  3. Reject spoofing
    Discard attacker-controlled addresses to the left of that boundary.
  4. Derive key
    Normalize IPv4 or group IPv6 by prefix before consulting the store.

#Quick Start

rateLimit() is an Elysia plugin — wire it with .use() and every request is gated before it reaches your handlers. The default key is the requester's IP; the default algorithm is GCRA.

TS
import { Elysia } from 'elysia';
import { rateLimit, gcra } from '@absolutejs/rate-limit';

new Elysia()
  .use(rateLimit({
    // GCRA — the algorithm Stripe uses. Exact, O(1) memory per key,
    // no boundary effects. Burst of 5, sustained 10 req/s.
    algorithm: gcra({ requestsPerPeriod: 10, periodMs: 1000, burst: 5 }),
    // 'ip' uses extractIp() with the trustedProxies + ipv6Prefix below.
    key: 'ip',
    trustedProxies: 1, // honor one CDN/LB hop of X-Forwarded-For
  }))
  .get('/', () => 'ok')
  .listen(3000);

Over the cap, the plugin returns 429 with Retry-After + the IETF RateLimit-* headers. The remaining count stays current on the 200 responses too — so clients can throttle themselves before they get refused.

#Algorithms

Pick GCRA unless you have a reason not to. It's exact, O(1) memory per key (one BigInt — the TAT), no boundary effects, no float drift. Token bucket and sliding-window are bundled for when you specifically want their semantics (burst-then-throttle or N-in-the-last-M-seconds, respectively). Stack any of them with combined.

TS
// GCRA — default. Exact, BigInt nanosecond TAT, no float drift.
import { gcra } from '@absolutejs/rate-limit';
gcra({ requestsPerPeriod: 100, periodMs: 60_000, burst: 20 });

// Token bucket — classic. Allows refill-boundary bursts up to capacity.
import { tokenBucket } from '@absolutejs/rate-limit';
tokenBucket({ capacity: 100, refillPerSecond: 1.667 });

// Sliding-window counter — approximation; intuitive on a status page.
import { slidingWindow } from '@absolutejs/rate-limit';
slidingWindow({ requestsPerPeriod: 100, periodMs: 60_000 });

// Combined — passes only when every component passes. Stack limits:
//   100 / minute per IP AND 10,000 / day per user-id
import { combined } from '@absolutejs/rate-limit';
combined({
  algorithms: [
    gcra({ requestsPerPeriod: 100,    periodMs: 60_000,     burst: 20 }),
    gcra({ requestsPerPeriod: 10_000, periodMs: 86_400_000           }),
  ],
});
  • GCRA — exact, BigInt nanosecond TAT, one number of state per key, no boundary effects. The default.
  • Token bucket — classic. Allows brief bursts at refill boundaries. Pick this when you want "fill the bucket, fire it all at once" semantics.
  • Sliding window — counter approximation. Easy to explain on a status page ("you have N requests left in the last M seconds").
  • Combined — composes any number of algorithms; passes only when every component passes. Stack a per-IP minute cap with a per-user-id daily cap in one plugin.

#Cost, Keys & Hooks

Heavy endpoints charge more than cheap ones with a cost function. The bucket goes into overdraft on a cost-N hit (you wait it off — same as Stripe's metered approach). Key derivation is a string preset ('ip' or 'authorization') or a function. skip bypasses the limit entirely; onAllow and onLimit are symmetric hooks for metrics + custom responses.

TS
new Elysia().use(rateLimit({
  algorithm: gcra({ requestsPerPeriod: 100, periodMs: 60_000, burst: 20 }),

  // Per-route cost — heavy endpoints charge more.
  cost: (ctx) => {
    if (ctx.request.url.includes('/upload')) return 5;
    if (ctx.request.url.includes('/admin'))  return 0; // free for admin
    return 1;
  },

  // Custom key derivation — per-tenant, falling back to IP.
  key: (ctx) => ctx.request.headers.get('x-tenant') ?? extractIp({
    connectionIp: ctx.server?.requestIP?.(ctx.request)?.address ?? null,
    headers: ctx.request.headers,
    trustedProxies: 1,
  }),

  // Skip for admin tokens (sync — async work belongs in key).
  skip: (ctx) => ctx.request.headers.get('authorization') === ADMIN,

  // Fire a billing event on every allowed request.
  onAllow: (_ctx, info) => meter.record({
    type: 'handler', tenant: info.key, durationMs: 0, cpuMs: 0, ok: true,
  }),

  // Customize the 429 response.
  onLimit: (_ctx, info) => new Response(JSON.stringify({
    ok: false, retryAfterSec: info.decision.retryAfterSec,
  }), { status: 429, headers: { 'Content-Type': 'application/json' } }),
}));

#IETF Headers

The default header set is IETF draft-09 — a combined RateLimit header carrying limit, remaining, and reset together, plus RateLimit-Policy describing the policy. Set headers: 'legacy' for the older X-RateLimit-* form (GitHub circa 2014), 'both' for a transition period, or false to suppress everything except Retry-After.

BASH
# IETF draft-09 (default — headers: 'standard')
RateLimit: limit=20, remaining=18, reset=15
RateLimit-Policy: 100;w=60;burst=20
Retry-After: 7                       # only on 429

# Legacy GitHub-style (headers: 'legacy')
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 18
X-RateLimit-Reset: 15
Retry-After: 7

# Or 'both' to emit both — useful during a transition.
# 'false' suppresses everything except Retry-After.

#IP & Proxy Trust

Most rate-limit libraries either trust X-Forwarded-For blindly (spoofable) or ignore it entirely (broken behind any CDN). trustedProxies: N honors only the last N hops — anything to the left is attacker-supplied and ignored. 0 disables XFF; 1 is the right default behind a single CDN/LB.

IPv6 addresses are grouped by their /64 prefix by default — one user's RIR allocation gets one quota, not one quota per device. Configurable via ipv6Prefix. CDN headers (cf-connecting-ip, fly-client-ip, true-client-ip, x-real-ip) are honored when XFF is missing and a proxy is trusted.

TS
// trustedProxies — IPv6 /64 — CDN-aware
import { extractIp } from '@absolutejs/rate-limit/core';

// 0: ignore X-Forwarded-For entirely. Use the raw connection IP only.
extractIp({ trustedProxies: 0, ... });

// 1: trust ONE proxy hop. Take XFF[length - 1] (the originator). This is
// the right default behind ONE CDN. Anything to the left is attacker-set.
extractIp({ trustedProxies: 1, ... });

// 2: trust TWO hops (CDN + load balancer). Same idea, take XFF[length-2].
extractIp({ trustedProxies: 2, ... });

// IPv6 grouping: by default IPv6 is reduced to its /64 prefix — one user
// allocation per RIR convention. 2001:db8::1 and 2001:db8::dead:beef both
// hash to '2001:db8:0:0:0:0:0:0/64'. Configurable via ipv6Prefix.
extractIp({ ipv6Prefix: 128, ... }); // disable grouping

// Honors cf-connecting-ip, fly-client-ip, true-client-ip, x-real-ip when
// XFF is missing and a proxy is trusted.

#Non-HTTP Usage

The algorithms + store are exported separately at @absolutejs/rate-limit/core with no Elysia dependency. Use them directly for WebSocket-message rate-limiting, queue-consumer throttling, AI-call quotas — anywhere you need exact rate enforcement without HTTP. Each algorithm exposes peek() for status-page read-only inspection and reset() for admin clears.

TS
// Use the algorithms directly outside HTTP — WebSocket message rate-limit,
// queue consumer throttle, AI call quotas. Import from /core to skip the
// Elysia dependency entirely.
import { gcra, memoryStore } from '@absolutejs/rate-limit/core';

const aiLimiter = gcra({ requestsPerPeriod: 60, periodMs: 60_000, burst: 10 });
const store = memoryStore();

async function callOpenAi(userId: string, prompt: string) {
  const decision = aiLimiter.check(store, userId, Date.now());
  if (!decision.allowed) {
    throw new Error(`Slow down — retry in ${decision.retryAfterSec}s`);
  }
  return openai.chat.completions.create({ ... });
}

// peek() = read-only inspection (no token consumed).
const remaining = aiLimiter.peek(store, userId, Date.now()).remaining;

// reset() = admin clear.
aiLimiter.reset(store, userId);

Continue toward an outcome

These playbooks show where this package fits, how to verify the combined system, and what changes before production.

Current package surface

What ships today

@absolutejs/rate-limitv0.5.1 · betaPlatform & InfranpmSource
6entry points52symbols

Import surface · click to copy

24 symbols
AlgorithmexportPermalinkSource
TS
Algorithm
Exported from @absolutejs/rate-limit
Use this API in an outcome:Prove a safe release

Outcomes

What you can build

Overview

First-class, from-scratch, 2026 rate limit for Bun + Elysia. GCRA by default (the algorithm Stripe uses — exact, O(1) memory per key, no boundary effects), IETF draft-09 RateLimit- headers, IPv6 /64 grouping, X-Forwarded-For trust modes, BigInt nanosecond TAT (no float drift), pluggable store (LRU in-memory bundled).

Why not just use the existing libraries

The leader in the Elysia ecosystem is rayriffy/elysia-rate-limit. It uses a token bucket over a Map with a setInterval cleanup. That's fine, but every choice in it is from 2020. The 2026 from-scratch answers:

Surface

Elysia plugin (@absolutejs/rate-limit)

Hardening checklist

Production guidance

Make every external boundary explicitPin the deployed @absolutejs/rate-limit version, replace example or memory-backed dependencies with durable implementations, bound external calls, protect credentials, and emit enough evidence to retry or recover safely.

Follow in order

Troubleshooting path

1
Trace from the first failed boundary
Reproduce the smallest canonical @absolutejs/rate-limit example, confirm the supported entry point and version in the API explorer, then inspect the first boundary that did not produce its documented result.