Day 24 of 100·Project Deep-Dive Day·454 words

Rate limiting as actually implemented — token buckets and windows

The exact limiters in both codebases, their keys, their flaws, and the upgrade path.

Know your own system

Zoth's token bucket (in-memory, per-tenant)

internal/middleware/ratelimit.go: per-tenant token buckets in a map with a background cleanup loop (every 10 min) evicting stale tenants. Token bucket semantics: capacity burst + steady refill rate — O(1) check, per-key memory. Status: implemented, unit-tested (4 tests), NOT mounted in the router. Be ready to say that exactly — "wired but not enabled at demo scale; enabling is one r.Use" — because an interviewer who greps will find it, and finding it before they do is the whole bulletproofing game.

Keys: tenant ID (from JWT context) — the right principal for a B2B product (limits organizations, not humans). For a public API you'd key by IP+user+route.

Traverse's limiters (express-rate-limit, in-memory)

From src/middleware/rateLimit.ts:

LimiterWindow:MaxScope
strictLimiter15 min : 5login, register, change-password, password-reset
generalLimiter1 min : 100all /api/*
authLimiter1 min : 30defined, unmounted
reconLimiter15 min : 60/api/recon/*

Configuration details worth citing: trust proxy 1 + validate: {xForwardedForHeader: false} (don't trust client-supplied XFF beyond the one real proxy), and isWhitelistedUser skipping identifiers matching the owner's handles. Fixed-window counters (express-rate-limit default): simple, but boundary-bursty — 200 requests in 2 s straddling a window edge pass. Sliding-window or token bucket smooths that (the sliding-window algorithm you'd implement: Redis ZSET of timestamps per key, ZREMRANGEBYSCORE + ZCARD + TTL — be able to sketch it; it's a top-5 interview question and your archive has a full page on it).

The honest architecture critique (say it first)

Both implementations are per-process, in-memory. At 1 replica: correct. At 2+: limits double, buckets fragment, and IP-based keys get inconsistent per pod. The upgrade is shared state: Redis token bucket (INCR with TTL, or Lua for atomic refill math) or a gateway-level limiter. Zoth's is already per-tenant keyed — swapping the map for Redis is a localized change. This exact critique is what "rate limiter placement and keys" questions are fishing for.

Where rate limiting sits in the defense-in-depth stack

Rate limiting is abuse prevention, one layer among: bcrypt-12 password hashing + 6-digit reset codes (15 min TTL, 5 attempts, bcrypt-hashed, PasswordResetCode), constant-time token compares where it matters (crypto.timingSafeEqual on recon tokens; known gap: Razorpay webhook compares with !== — flag it yourself), strict auth ordering. Placement principle: limit as close to the edge as possible (before expensive work: auth parse, DB), key on the smallest stable principal (user > IP; tenant for B2B), and fail open for availability on the limiter itself or closed for security-sensitive endpoints — a decision, either way, stated.

Bulletproof your resume

Say these answers aloud before moving on

These are the follow-ups an interviewer will actually ask about your projects.

01

Why token bucket over fixed window?

Fixed windows burst at boundaries — 2× the limit can slip through straddling a reset. Token buckets enforce a steady rate while allowing a controlled burst up to capacity, with O(1) check and per-key memory. Zoth's limiter is a bucket; traverse's express-rate-limit default is fixed-window, which I accepted for its simplicity and would swap to a Redis sliding window (ZSET) or bucket if boundary gaming ever mattered.

02

How would you make traverse's limiter multi-replica safe?

Move counters to Redis: fixed window via INCR+EXPIRE (one atomic op), sliding window via a ZSET of timestamps with ZREMRANGEBYSCORE to the window start, or token bucket via a Lua script for atomic refill math. Keep the middleware API identical so routes don't change. The design decision that matters: the limiter must fail open if Redis is unreachable (availability over abuse), unless the endpoint is security-critical (auth), where I'd fail closed.

03

Why is your recon token comparison constant-time but your payment webhook's not?

The recon token check uses crypto.timingSafeEqual because it's a static bearer token — timing leaks would let an attacker recover it byte-by-byte. Razorpay's webhook verifies an HMAC signature over the body — comparing digests with !== leaks, in principle, timing on the first differing byte, but recovering a full HMAC-SHA256 signature via timing remotely is impractical versus a pre-shared-token equality check. Still, the fix is one line (swap to timingSafeEqual), and I flag it because 'I know which of my comparisons are timing-sensitive' is the actual signal.