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:
| Limiter | Window:Max | Scope |
|---|---|---|
| strictLimiter | 15 min : 5 | login, register, change-password, password-reset |
| generalLimiter | 1 min : 100 | all /api/* |
| authLimiter | 1 min : 30 | defined, unmounted |
| reconLimiter | 15 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.
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.
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.
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.