Benchmarks
Methodology, reference hardware, and reproducible performance numbers for the standardized benchmark suite.
RateLock implements each storage engine natively rather than wrapping a generic database layer. Redis uses Lua scripts for atomic counters and sorted sets, PostgreSQL uses targeted UPSERT queries, and the local memory engine uses JavaScript Map lookups. This page shows the cost of those choices under a controlled workload.
Hardware and runtime dependency
The numbers on this page come from a single reference machine. They are not a guarantee of throughput or latency in your environment. Use them to compare strategies and backends against each other, not to predict production performance. Re-run the suite on your target hardware before making sizing decisions.
What this suite measures
For every (adapter, scenario) pair, the harness runs a fixed-duration workload of concurrent workers calling the rate-limit check, then collects:
- Throughput in operations per second, computed from the total number of completed iterations.
- Latency percentiles (p50, p95, p99) of the wall-clock time from issuing the check to receiving the response.
- Allowed rate, the percentage of iterations that the limiter returned as
allowed: true. Indiverse-keysscenarios this sits at 100% (one request per key). Inextreme-spamscenarios, the limit is exhausted quickly and most iterations are denied.
The numbers reported are the median across 3 runs of 2 seconds each, with a 500 ms warmup using a different key space so the V8 inline caches, JIT, and connection pools are hot before measurement starts. The raw JSON output also records the per-run min, max, and stdev so you can see the spread.
What this suite does not measure
- Cold start or first-request latency. Warmup is explicit.
- Network latency to remote backends. All backends run in local Docker containers, so the round-trip is the minimum the OS can deliver.
- V8 GC pauses. By default the runner does not expose
--expose-gc. Abench:full:gcvariant exists that does, for users who want GC-controlled numbers. - Multi-process or clustered deployments. The harness runs a single Node.js process.
- Storage cost over time. Memory growth, WAL size, and disk pressure are not part of the throughput/latency story.
- Failure modes in production. Connection retries, partial failures, and DNS resolution are out of scope.
Reference hardware
The committed numbers were generated on:
- CPU: AMD Ryzen 7 5800X (8 cores, 16 threads)
- RAM: 32 GB DDR4
- OS: Linux x64, kernel 6.x
- Node.js: v25.9.0 (Bun 1.3.14 is also tested; see the Runtimes page for cross-runtime notes)
- Backends: Postgres 18, Redis 8, Valkey 8, all running in Docker on the same host
Your throughput will scale roughly with single-core CPU frequency and memory bandwidth. Latency to a remote backend will be dominated by network round-trip, which the local Docker setup does not capture. The committed numbers on this page are Node.js; Bun numbers are in the same ballpark with a small per-scenario spread documented on the Runtimes page.
Methodology in detail
Workload
- 80 concurrent workers per scenario (
BENCH_CONCURRENCY=80). - 2 second timed phase per run (
BENCH_DURATION=2000). - 3 runs per
(adapter, scenario)pair (BENCH_RUNS=3). The reported number is the median; the JSON also carries min, max, and stdev. - 500 ms warmup (
BENCH_WARMUP_MS=500) on a different key space using theDiverse Keysscenario, so the rate-limit quota is not consumed before measurement.
Scenarios
| Scenario | Shape | What it stresses |
|---|---|---|
| Diverse Keys | Every iteration uses a fresh key | Hot path of cache and storage; no contention on a single key |
| Extreme Spam | 80 workers hammer the same key until the 1000 req / 60 s limit is exhausted | Worst case for a single key: every check after the first 1000 is denied |
| Realistic Mix | 30% of iterations target one hot key, 70% target fresh keys | Mixed load that resembles real traffic |
| Batch Check | Each iteration checks an array of 5 identifiers in parallel | Per-iteration overhead when multiple keys are submitted together |
For this page, only Diverse and Extreme Spam numbers are shown. Realistic Mix and Batch are present in the raw output.
Backends and tunings
All backends run in local Docker containers. The Postgres container in packages/bench/docker-compose.yml runs with production-default durability (synchronous_commit=on, fsync=on, full_page_writes=on); only sizing tunings are applied (max_connections=200, shared_buffers=512MB). The numbers on this page are therefore what you would see on a real Postgres deployment, not on a synthetic best-case config. The trade-off is a higher run-to-run σ on Postgres than on Redis or local memory, because the WAL is now part of the per-check cost.
Reproducing older numbers (best-case config)
Earlier drafts of this page used a synthetic Postgres config with synchronous_commit=off, fsync=off, and full_page_writes=off to isolate the driver/protocol cost. That config is preserved in the focused investigation script at packages/bench/src/scripts/investigate-unlogged.ts, which can target any Postgres container via the POSTGRES_URL env var. The bench itself, however, runs on production defaults.
Redis and Valkey run with their default durability settings. The Redis client used by both RateLock and rate-limiter-flexible in the comparison matrix is ioredis, so any throughput gap is the limiter implementation and not the client.
Rate-limiter-flexible integration
rate-limiter-flexible throws on rate-limit denial. The RLF adapter wraps each call in try/catch to mirror how a real application would consume it, then maps to a { allowed: boolean } shape. RateLock returns a value rather than throwing, so the comparison reflects the actual public API cost of each library.
Local Memory Strategies
Strategies that fit in process memory. Counter-based strategies run in constant time per check; the sliding window strategy maintains a per-key array of timestamps and runs in $O(N)$ per check.
| Strategy | Diverse (median ops/sec) | p99 latency | Extreme Spam (median ops/sec) | p99 latency |
|---|---|---|---|---|
| Fixed Window | 786,410 | 0.29 ms | 2,349,639 | 0.04 ms |
| Token Bucket | 1,159,155 | 0.08 ms | 2,249,806 | 0.04 ms |
| Individual Fixed Window | 1,101,831 | 0.10 ms | 2,460,081 | 0.04 ms |
| Sliding Window | 723,922 | 0.15 ms | 75,676 | 1.98 ms |
Local memory is unaffected by the Postgres durability switch. Token Bucket and Individual Fixed Window lead on diverse keys; under extreme spam, every counter-based strategy saturates around 2.3-2.5M ops/sec because the only work is a Map.get and an integer increment. Sliding Window is stable but slow: every check iterates the per-key timestamp array, which is full after 60 s of spam.
Choosing a memory strategy
For a hot key with high request volume, prefer Token Bucket or Individual Fixed Window. Fixed Window is fine when the boundary effect is acceptable. Sliding Window is the only one that gives true rolling-window semantics; the cost is roughly 30x lower throughput in this scenario.
Redis Strategies
All numbers below use ioredis and connect to a local Redis 8 container.
| Strategy | Diverse (median ops/sec) | p99 latency | Extreme Spam (median ops/sec) | p99 latency |
|---|---|---|---|---|
| Fixed Window | 139,791 | 0.97 ms | 141,720 | 0.99 ms |
| Token Bucket | 120,255 | 0.94 ms | 139,932 | 0.98 ms |
| Individual Fixed Window | 132,027 | 1.03 ms | 147,620 | 0.91 ms |
| Sliding Window | 100,511 | 1.09 ms | 119,748 | 1.23 ms |
The Redis numbers are an order of magnitude below the in-memory numbers because every check is a network round-trip. The p99 latency in the diverse scenario is dominated by the network and serialization, not by the algorithm choice. Under spam, the work shifts to the limiter's Lua script rather than the network, so latency compresses.
PostgreSQL Strategies
Postgres numbers come from a local Postgres 18 container with production-default durability (see Methodology in detail). Throughput is two orders of magnitude below Redis because of the per-query transaction cost.
| Strategy | Diverse (median ops/sec) | p99 latency | Extreme Spam (median ops/sec) | p99 latency |
|---|---|---|---|---|
| Fixed Window | 32,013 | 5.00 ms | 30,858 | 5.18 ms |
| Sliding Window | 27,430 | 6.20 ms | 19,340 | 8.76 ms |
| Token Bucket | 23,879 | 6.79 ms | 1,795 | 229.95 ms |
| Individual Fixed Window | 29,312 | 5.75 ms | 24,924 | 6.33 ms |
The diverse scenario is calm: all four strategies sit between 24K and 32K ops/sec. The extreme-spam scenario is where the choice matters:
- Fixed Window, Sliding Window, and Individual Fixed Window stay close to their diverse throughput. They are UPSERT-friendly and tolerate single-key contention. Sliding Window is the slowest of the three under spam but it is no longer the cliff it used to be. The named prepared statement optimisation closed the plan-cache gap that used to dominate.
- Token Bucket collapses to ~1,800 ops/sec with 200+ ms p99. A transaction per check, plus a refill calculation, is too much for a hot Postgres row to absorb.
Postgres strategy recommendation
On Postgres, prefer Fixed Window or Individual Fixed Window. Avoid Sliding Window and Token Bucket for high-volume single-key workloads. If you need rolling-window semantics, consider the withCache decorator (see section 5) to absorb the spam in memory.
Logged vs Unlogged tables
| Table type | Diverse (median ops/sec) | p99 latency |
|---|---|---|
| Logged (default) | 22,379 | 6.01 ms |
| Unlogged | 26,837 | 5.89 ms |
On a production-default Postgres, unlogged tables are 20% faster (26,837 vs 22,379 ops/sec) on the diverse scenario. This is the gap that earlier drafts of this page were missing: the bench was running with all durability disabled (synchronous_commit=off, fsync=off, full_page_writes=off), which made WAL effectively free and the gap shrank to noise. With the bench now using production defaults, the gap is real and consistent with the deeper investigation across PG 14, 15, and 18.
A focused investigation across PostgreSQL versions and durability configs is preserved at results/wal-overhead-investigation.md, runnable via packages/bench/src/scripts/investigate-unlogged.ts:
| Postgres version | Durability config | Logged (ops/s) | Unlogged (ops/s) | Ratio | WAL overhead |
|---|---|---|---|---|---|
| 18.4 | all off (synthetic best case) | 29,244 | 30,303 | 1.04x | -4% (noise) |
| 18.4 | sync_commit=off only | 29,517 | 30,392 | 1.03x | -3% (noise) |
| 18.4 | all on (production, this bench) | 25,127 | 28,795 | 1.15x | -15% |
| 15.18 | all on (production) | 25,152 | 27,008 | 1.07x | -7% |
| 14.23 | sync_commit=off only | 25,609 | 26,613 | 1.04x | -4% |
| 14.23 | all on (production) | 26,676 | 29,591 | 1.11x | -11% |
On production-default Postgres, unlogged gives 7-20% higher throughput depending on version. On a tuned synchronous_commit=off deployment, the gain shrinks to 3-4%. Use unlogged for the same reason you would use an in-memory cache: the data is acceptable to lose on crash. If your cluster must survive crashes with no data loss, stay on logged tables.
RateLock vs rate-limiter-flexible
This matrix is the one most likely to be misread, so the methodology matters. Both libraries are configured with points: 1000, duration: 60s (RLF's units) or limit: 1000, windowMs: 60000 (RateLock's). Both run on the same hardware with the same backends. The Redis client is ioredis for both, as discussed in Methodology in detail. The Postgres container uses production-default durability in this matrix. See the note at the top of section 3.
| Backend | RateLock (median ops/sec) | RLF (median ops/sec) | Ratio |
|---|---|---|---|
| Local Memory (extreme spam) | 2,088,809 | 744,508 | 2.81x |
| Redis (extreme spam, ioredis) | 142,637 | 83,403 | 1.71x |
| PostgreSQL (extreme spam) | 29,330 | 28,171 | 1.04x |
- On local memory, RateLock is 2.8x faster under spam. The single-key hot path benefits from direct
Maplookups and the absence of per-call class instantiation. - On Redis, RateLock is 1.7x faster. Both libraries use Lua scripts for atomic counters, so the gap is the script body and the network framing around it.
- On PostgreSQL, the two libraries are near-identical under spam in this run (1.04x). Both run a single UPSERT per call against the same hot row. RateLock's
pgDriveruses named prepared statements (FNV-1a hash of the SQL text), which closed the plan-cache gap that earlier versions of this matrix showed. Treat any claim about a stable Postgres gap as a measurement, not a guarantee. The numbers move with the bench environment.
API differences that bias the comparison
rate-limiter-flexible.consume() throws on denial. The RLF adapter wraps each call in try/catch to mirror real usage. RateLock returns a value rather than throwing, so it does not pay the cost of exception handling. Under extreme spam where 99.9% of calls are denied, the try/catch is a real cost that this number reflects.
Strategy availability
RLF implements only the Fixed Window strategy for Redis and Postgres in the comparison. RateLock implements all four strategies natively on all three backends. The 1.7x Redis gap is on Fixed Window, which is the only like-for-like comparison available.
Resilience Policies (Decorator Overhead)
The decorators (withCache, withCircuitBreaker, withFallback, withRetry) are designed to add value on top of remote backends: withCache absorbs hot-key spam in memory, withCircuitBreaker stops hammering a failing backend, withFallback swaps to a secondary when the primary is down, withRetry masks transient errors. Wrapping a local limiter is structurally pure overhead: there is no network round-trip to absorb, no failure to circuit-break, nothing to retry.
This section is therefore split in two with very different intents:
- 5.1 Decorator on a local Fixed Window (raw overhead reference): how much each decorator costs per call when the underlying limiter is already local. This is a reference for the per-call cost of the decorator machinery, not a recommendation of whether to use it.
- 5.2 Decorator on Redis under extreme spam (the value matrix): the production-realistic case where the decorators earn their keep.
5.1 Raw overhead reference (decorator on local)
| Limiter | Diverse (median ops/sec) | p99 latency | Relative to raw |
|---|---|---|---|
| Raw Fixed Window | 1,137,689 | 0.09 ms | reference |
| + withCache | 1,036,213 | 0.10 ms | -9% |
| + withCircuitBreaker | 1,077,556 | 0.09 ms | -5% |
| + withFallback | 1,027,640 | 0.10 ms | -10% |
| + withRetry | 1,027,654 | 0.09 ms | -10% |
All four decorators add overhead on a local limiter; the run-to-run spread is large because the local limiter is fast enough that small differences in JIT optimisation flip the ordering. Read this as the per-call cost of the decorator machinery. On a local limiter, every decorator is pure overhead because there is no remote backend to protect.
Section 5.2 below is the one that actually matters.
5.2 Value under extreme spam (decorator on Redis)
Under extreme spam on a single key, the 1000 req/60s limit is exhausted within the first few milliseconds of the run. After that, every call hits Redis just to learn "denied", and Redis becomes the bottleneck at ~140K ops/sec. The withCache decorator absorbs those denied responses in memory and turns the limiter into a local-memory operation for the rest of the run.
| Limiter | Extreme Spam (median ops/sec) | p99 latency | vs raw |
|---|---|---|---|
| Raw Redis Fixed Window | 143,059 | 1.11 ms | reference |
| Redis + withCache | 2,320,245 | 0.04 ms | 16.2x faster |
| Redis + withCircuitBreaker | 139,674 | 0.97 ms | -2% (no failures to break) |
| Redis + withRetry | 139,763 | 1.00 ms | flat (no transient errors to retry) |
withCache delivers 2.32M ops/sec vs 143K: a 16.2x throughput gain and a 28x latency drop (1.11 ms → 0.04 ms). The cache does this by remembering the last "denied" result for each key for 100 ms; under extreme spam on a single key, every call after the first 1000 is served from memory instead of going through Redis.
withCircuitBreaker and withRetry show no benefit on this matrix because Redis is healthy. Their value lives in the failure case, which is not part of the standard bench. Section 5.3 below injects Redis failures and measures the circuit-breaker / retry / fallback payoff.
Practical recommendation
- Always wrap a remote limiter in
withCacheif the limiter can face a hot-key flood. The cache pays for itself in the first millisecond of any sustained spam and turns a Redis-bound denial path into a local-memory one. - Wrap in
withCircuitBreakerif Redis can fail (network blip, server restart, cluster failover). The cost is ~2% on a healthy backend; the benefit is the backend being able to recover instead of being hammered. - Wrap in
withFallbackif you have a secondary limiter (typically local) and availability matters more than global consistency. The fallback activates only when the primary fails, so the cost on the happy path is just the activation check (~7%). - Wrap in
withRetryonly if you are seeing transient errors on the limiter call. The cost is ~4% on the happy path; on a healthy backend, retries add nothing and only delay failure propagation. - Skip all decorators on a local limiter. The local limiter is already as fast as the cache, so the decorator is pure overhead.
- On a remote limiter,
withFallbackis the decorator that survives a hard outage. See 5.3 for the numbers: when Redis is hard-down,withFallbackis the only decorator that keeps the service accepting traffic at 100% allowed rate.
5.3 Value under failure (decorator on Redis with fault injection)
Section 5.2 shows what happens on a healthy backend. Production deployments are not healthy. To measure the value of the failure-recovery decorators in realistic conditions, this matrix wraps a real Redis client in a Proxy that injects three kinds of failures before delegating to the real client. The errors are indistinguishable from real Redis failures (same error type, same latency profile), so the limiter and decorators react the same way they would in production.
Three failure profiles, run against the same extreme-spam workload as 5.2.
5.3.1 Transient errors (10% of calls throw)
Each call has a 10% chance of throwing a transient error at the Redis client level, before the limiter logic sees it.
| Limiter | Throughput (ops/sec) | Allowed count | Allowed % | vs raw |
|---|---|---|---|---|
| Raw Redis | 137,539 | 1,000 | 0.12% | reference |
| + withRetry | 117,343 | 1,000 | 0.14% | -15% (slower, same result) |
| + withCircuitBreaker | 86,669 | 146 | 0.03% | -37% (trips after 3 errors) |
| + withFallback | 133,743 | 81,732 | 10.18% | -3% throughput, errors hidden |
Two results worth flagging:
withRetryis slower than raw, not faster. Under extreme spam the limiter denies 99.9% of calls within the first few milliseconds. Re-running a denied call does not change the answer; it just adds latency. Retry only helps when the answer would have been different, and a "denied" decision from the rate limiter is final.withCircuitBreakeris the worst outcome: 146 allowed (vs 1,000 for raw). The breaker opens after 3 errors and starts fast-failing, which means even requests that the limiter would have allowed are now blocked. The breaker protects the backend, not the user, and 10% transient errors is exactly the regime where it is too aggressive for a limiter that already denies most traffic.
withFallback is the only decorator that hides the errors. It catches the 10% of calls that would have thrown and returns the configured fallback (allowed, with a synthetic remaining/reset). The user sees 10.13% allowed because the fallback bypasses the rate-limit ceiling for failed calls only.
5.3.2 Slow Redis (50 ms per call)
All calls succeed but take 50 ms. This is the latency-spike scenario: a slow backend, not a failing one.
| Limiter | Throughput (ops/sec) | p99 latency | vs raw |
|---|---|---|---|
| Raw Redis | 1,570 | 50.91 ms | reference |
| + withRetry | 1,572 | 50.92 ms | flat |
| + withCircuitBreaker | 1,572 | 50.92 ms | flat |
Every decorator converges to the same number: ~1,570 ops/sec, 50.92 ms p99. The throughput is bound by the 50 ms latency per call, and none of the three decorators helps because they are all triggered by errors, not by latency. Retry only re-runs on errors; circuit-breaker only opens on errors; fallback only activates on errors. A slow-but-successful backend looks identical to a healthy one to all three.
Known gap: a future withTimeout(p, { budgetMs: 10 }) decorator would let a caller abort a slow call and treat it as a failure, at which point the existing retry / circuit-breaker / fallback stack would react correctly. The current benchmarks do not include this decorator.
5.3.3 Hard down (100% of calls fail)
Every call throws. This is the worst-case scenario: total Redis outage.
| Limiter | Throughput (ops/sec) | Allowed count | Allowed % | vs raw |
|---|---|---|---|---|
| Raw Redis | 131,050 | 0 | 0.00% | reference |
| + withRetry | 18,043 | 0 | 0.00% | -86% (retries 3x, still fails) |
| + withCircuitBreaker | 84,658 | 0 | 0.00% | -35% (trips fast, no recovery) |
| + withFallback | 121,419 | 727,023 | 100.00% | flat, errors hidden |
When Redis is hard-down, raw, withRetry, and withCircuitBreaker all serve 0 requests. withFallback serves 727,023 of them at near-baseline throughput (121,419 ops/sec vs 143,059 for raw-on-healthy, a 15% gap that is the cost of the try/catch wrapping). The fallback policy is "allow" with a local synthetic result, so the service keeps accepting traffic and the user-facing system stays up.
withRetry is the worst choice in this scenario: 86% slower than raw, because it tries the failing call 3 times in series (baseDelayMs: 2, maxDelayMs: 10) before giving up. withCircuitBreaker is faster than retry but still serves nothing, because opening the circuit means every subsequent call returns the configured "deny" without consulting the backend. There is no fallback configured for the breaker.
5.3.4 Verdict
The 5.3 matrix inverts the conclusion of 5.2. On a healthy backend, withCache is the only decorator that earns its keep. On a degraded backend, withFallback is the only decorator that earns its keep. The two decorators are complementary: withCache is for the hot-key flood, withFallback is for the backend outage. A production-grade remote limiter should ship with both.
withCircuitBreaker and withRetry still have a place, just not the one this matrix exercises. The circuit-breaker protects a backend from a tight retry loop; retry smooths out one-off blips on a primary before the fallback activates. Both belong in front of the primary when the primary feeds a withFallback. On their own, with a single-tier Redis backend, they cost more than they save.
Redis and Valkey Client Drivers
Both Redis and Valkey are tested with the ioredis and node-redis clients under extreme spam. The numbers in this section come from a freshly-pulled Redis 8.8.0 and Valkey 8.1.8 container, so the comparison is across same-major server versions on both engines.
| Backend / client | Median ops/sec | p99 latency |
|---|---|---|
| Redis 8 (ioredis) | 144,140 | 0.93 ms |
| Redis 8 (node-redis) | 141,776 | 0.95 ms |
| Valkey 8 (ioredis) | 137,709 | 0.96 ms |
| Valkey 8 (node-redis) | 143,296 | 0.83 ms |
All four rows sit within a 5% band. ioredis was used for the package-comparison matrix to keep the two libraries on equal footing. The limiter implementation is the dominant cost in this scenario, not the client.
Postgres drivers: pg vs postgres.js
Both drivers ran cleanly on the diverse scenario under production-default durability, with postgres.js ahead on Fixed Window (33,025 vs 22,872 logged / 28,183 unlogged) and on Token Bucket (23,853 vs 19,135). Under extreme Token Bucket spam, the two drivers converge to roughly the same throughput (~1,700 ops/sec, ~213-223 ms p99). The bottleneck is the single hot row, not the driver. The earlier draft of this page reported that postgres.js failed with UNSAFE_TRANSACTION under the default concurrency of 80; the root cause was sql.unsafe() at the top level on a pool with max > 1, which postgres.js v3 disallows. The fix was to use sql.reserve() inside the driver to checkout a dedicated connection per query, allowing the pool to run at full concurrency. As of v0.2, each strategy in @ratelock/postgres ships with two implementations: one using pg's native (sql, params) API with named prepared statements, and one using postgres.js's native tagged templates. Each driver runs on its own optimal path.
How to reproduce
The benchmark suite lives in packages/bench/. The full suite takes about 90 seconds on the reference machine and writes its results to packages/bench/results/.
git clone https://github.com/saoudi-h/ratelock.git
cd ratelock
pnpm install
# Standard run, 3 measurements per scenario
pnpm --filter @ratelock/bench bench:full
# Same run with explicit GC control between scenarios
pnpm --filter @ratelock/bench bench:full:gc
# Pin to a single CPU core for reproducibility
pnpm --filter @ratelock/bench bench:fair
# Tweak workload
BENCH_DURATION=5000 BENCH_RUNS=5 BENCH_CONCURRENCY=40 \
pnpm --filter @ratelock/bench bench:fullThe JSON output (results/benchmarks_raw.json) carries the per-run min, max, and stdev, which is the easiest way to verify that a run is stable. The Markdown report (results/benchmark_report.md) has the same tables that the console prints.
Run on idle hardware
Co-tenant processes will perturb the numbers, especially the Postgres and Redis measurements, which sit in the 100-microsecond-to-millisecond range. Close browsers, pause CI, and disable turbo boost for a more stable run.
Limitations and known gaps
- The harness does not model connection-pool exhaustion. A 200-connection pool under 80 concurrent workers is comfortable, but a real application with hundreds of workers can hit the pool ceiling and turn into a queue.
- The Postgres container in the suite runs with production-default durability (
synchronous_commit=on,fsync=on,full_page_writes=on). Numbers from this page are representative of a real Postgres deployment. A focused investigation with all-durability-off and across PG 14, 15, 18 is preserved inresults/wal-overhead-investigation.md. - The
Realistic MixandBatch Checkscenarios are present in the harness and in the raw JSON, but not on this page. They add two more cross-sections (a mix of hot and cold traffic; multi-key per iteration) and the conclusions do not change in a way that would alter the strategy recommendation. - The RLF comparison uses the Fixed Window strategy on every backend, because RLF does not implement the other three strategies on Redis or Postgres. The 1.7x Redis gap is on the only like-for-like comparison available.
- Sliding Window and Token Bucket under spam on Postgres are measured but not recommended. The harness does not currently test the case where the Postgres
withCachedecorator shields those strategies from spam, which is the configuration you would actually use in production. - The decorator matrix in section 5.2 tests the happy path only.
withCircuitBreakerandwithRetryonly show their value when the backend is failing or returning transient errors. Section 5.3 covers the failure case: it wraps the real Redis client in a fault-injecting proxy and measures the decorator payoff under three realistic failure profiles (10% transient errors, 50 ms latency, 100% hard down). The headliners from that matrix:withFallbackis the only decorator that survives a hard outage;withCacheandwithFallbackare the two decorators that should always wrap a remote limiter.
How is this guide?
Last updated on