Interview loops keep accepting agent-drafted rate limiters that pass a happy-path fixture and then leak quota across customers. The failure is not missing Redis knowledge but treating a process-local counter as a finished multi-tenant contract. This packet gives reviewers a prompt, a rubric, a compact reference module, and graded failure modes. Hidden tests stay off the candidate gist so memorized fixtures cannot launder a global process counter.
Why this task still sorts candidates
Public coding tests for HTTP 429 handling are now widely memorized by coding models used in interviews. A candidate or an agent can emit a Map of IP addresses and a reset timestamp without understanding isolation. Reviewers therefore need a task whose public tests look ordinary while hidden invariants punish global keys. Fail-open stores and wall-clock coupling belong in that same hidden set because they survive fixture-driven generation.
This packet is written for JavaScript service interviews that already allow an assistant during the take-home. The instructions do not ban agents and do not grade the brand of editor the candidate used. They grade whether the finished module still enforces tenant boundaries after the assistant session ends.
Candidate prompt
Interviewers should copy the following block into the take-home and avoid adding extra product narrative. The public fixture below is the only test file the candidate is allowed to see. Hidden isolation cases stay in the interviewer's private checkout until scoring day.
Prompt text
Build a production-shaped HTTP rate limiter for a multi-tenant public API without talking to a real network. Each request carries tenantId, route, and an optional subject such as a user identifier from the caller. The limiter must decide allow or reject before the route handler runs and before any downstream side effect. Rejected calls return status 429 and a Retry-After header measured in whole seconds until the window ends.
Limits are per tenant and per route, never global across the process and never derived from peer IP alone. A noisy tenant must not starve another tenant that shares the same Node.js instance or worker. When the backing store throws, the limiter must fail closed and reject the request with 429. The clock must be injectable so tests can advance time without sleeping or stubbing Date.now.
Deliver createLimiter(store, clock, policies) together with a pure check(request) function that returns a decision object. Include tests for isolation, window expiry, fail-closed behavior, and Retry-After arithmetic on the injected clock.
Hard constraints
- Language is Node.js with no framework requirement and no HTTP listener in the scored module.
- Store is an injected interface exposing
get,set, andincronly. - Clock is
{ nowMs() }and must be the only time source inside window math. - Policies arrive as
{ route, windowMs, max }entries keyed by exact route strings. - Quota state must not live in a module-level
Map,Date.now(), orsetTimeout. - Logs must not print authorization headers, cookies, or raw bearer tokens.
Public fixture the candidate does see
// public-fixture.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createLimiter } from './limiter.js';
export function memoryStore() {
const m = new Map();
return {
async get(k) {
return m.get(k) ?? null;
},
async set(k, v) {
m.set(k, v);
},
async incr(k, n = 1) {
const cur = (m.get(k) ?? 0) + n;
m.set(k, cur);
return cur;
},
};
}
export function fakeClock(start = 1_000_000) {
let t = start;
return {
nowMs: () => t,
advance: (ms) => {
t += ms;
},
};
}
test('allows traffic under the published limit', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 2 },
]);
const req = { tenantId: 't1', route: 'GET /v1/items', subject: 'u1' };
assert.equal((await limiter.check(req)).allowed, true);
assert.equal((await limiter.check(req)).allowed, true);
});
The public fixture is intentionally thin so a memorized 429 snippet can still look complete on first run. Agents that stop after greening this file usually fail the hidden suite on tenant keys or store errors.
Scoring rubric
Reviewers should score the submitted module and the hidden tests, not README tone or extra backends. Style nits, TypeScript conversion, and unused Redis clients do not recover a missing tenant key. A passing packet needs isolation and fail-closed at full weight before any other row is discussed.
| Area | Weight | Pass | Partial | Fail |
|---|---|---|---|---|
| Tenant and route isolation | 30 | Keys include tenantId and route | Keys include only tenant or only IP | One process-wide counter |
| Time control | 15 | Uses injected clock only | Mixes clock and Date.now | Sleeps or reads wall clock |
| Fail closed | 20 | Store errors become 429 | Errors become 500 deny | Errors become allow |
| Retry-After math | 15 | Whole seconds until window end | Constant 60 | Missing or negative |
| Test quality | 20 | Hidden invariants covered | Only public fixture | No tests or snapshot fluff |
Reference implementation
The following sample is a teaching sketch for interviewers, not a library release. Keep the hidden tests out of the repository the candidate clones. Callers should treat allowed, status, and retryAfterSec as the only contract from check.
// limiter.js
export function createLimiter(store, clock, policies) {
const policyByRoute = new Map(policies.map((p) => [p.route, p]));
function keyFor(req, windowStart) {
return `rl:${req.tenantId}:${req.route}:${windowStart}`;
}
function windowStart(now, windowMs) {
return now - (now % windowMs);
}
function retryAfterSec(now, start, windowMs) {
return Math.max(1, Math.ceil((start + windowMs - now) / 1000));
}
async function check(req) {
if (!req || typeof req.tenantId !== 'string' || typeof req.route !== 'string') {
return { allowed: false, status: 429, retryAfterSec: 1 };
}
const policy = policyByRoute.get(req.route);
if (!policy) {
return { allowed: true, status: 200, retryAfterSec: 0 };
}
const now = clock.nowMs();
const start = windowStart(now, policy.windowMs);
const key = keyFor(req, start);
let count;
try {
count = await store.incr(key, 1);
if (count === 1) {
await store.set(`${key}:exp`, start + policy.windowMs);
}
} catch {
return {
allowed: false,
status: 429,
retryAfterSec: retryAfterSec(now, start, policy.windowMs),
};
}
if (count <= policy.max) {
return { allowed: true, status: 200, retryAfterSec: 0 };
}
return {
allowed: false,
status: 429,
retryAfterSec: retryAfterSec(now, start, policy.windowMs),
};
}
return { check };
}
Wrapping this module in Express or Fastify middleware is out of scope for the scored packet. Interviewers who want HTTP wiring can add it after the hidden suite is green.
Hidden tests interviewers should keep private
These cases are the actual filter. Do not paste them into the candidate prompt or into an assistant chat that the candidate can read.
// hidden.isolation.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createLimiter } from './limiter.js';
import { memoryStore, fakeClock } from './public-fixture.test.js';
test('does not spend tenant B quota on tenant A traffic', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 1 },
]);
const a = { tenantId: 't-a', route: 'GET /v1/items' };
const b = { tenantId: 't-b', route: 'GET /v1/items' };
assert.equal((await limiter.check(a)).allowed, true);
assert.equal((await limiter.check(a)).allowed, false);
assert.equal((await limiter.check(b)).allowed, true);
});
test('does not share budget across different routes', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 1 },
{ route: 'POST /v1/items', windowMs: 60_000, max: 1 },
]);
const getReq = { tenantId: 't1', route: 'GET /v1/items' };
const postReq = { tenantId: 't1', route: 'POST /v1/items' };
assert.equal((await limiter.check(getReq)).allowed, true);
assert.equal((await limiter.check(getReq)).allowed, false);
assert.equal((await limiter.check(postReq)).allowed, true);
});
test('fails closed when incr throws', async () => {
const clock = fakeClock();
const store = {
async get() {
return null;
},
async set() {},
async incr() {
throw new Error('redis down');
},
};
const limiter = createLimiter(store, clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 10 },
]);
const res = await limiter.check({ tenantId: 't1', route: 'GET /v1/items' });
assert.equal(res.allowed, false);
assert.equal(res.status, 429);
assert.ok(res.retryAfterSec >= 1);
});
test('opens a new window after injected time passes', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 1_000, max: 1 },
]);
const req = { tenantId: 't1', route: 'GET /v1/items' };
assert.equal((await limiter.check(req)).allowed, true);
assert.equal((await limiter.check(req)).allowed, false);
clock.advance(1_000);
assert.equal((await limiter.check(req)).allowed, true);
});
Run the public file first, then the hidden file, using the same working tree and the same limiter.js.
node --test public-fixture.test.js
node --test hidden.isolation.test.js
Common failure modes
Reviewers should watch for these patterns in agent output, because they green the public fixture while breaking tenancy.
- Global Map keyed by IP. The module compiles and the public test passes, yet tenant B is blocked after tenant A spends the only counter.
- Fail open on store errors. The generated comment says availability matters more than quota, which turns a cache outage into an unbounded bill.
-
Date.now()mixed into window math. Hidden tests that advance a fake clock never expire the window, so expiry cases false-fail or hang. -
Route omitted from the key.
GET /v1/itemsandPOST /v1/itemsshare one budget, which is a product defect in most public APIs. - Retry-After set to the full window. Clients wait a minute after a rejection that should clear in two seconds on the injected clock.
- Subject used as the only key. A missing subject disables the limit, and a stolen subject string throttles the wrong caller.
-
In-memory module state across workers. Two processes each allow
maxrequests, which silently doubles the published quota.
None of these failures require a malicious candidate or a broken compiler. They appear when an assistant optimizes for the visible fixture and stops.
Using an agent without laundering the grade
Teams that already let candidates use assistants should still freeze the hidden tests before the prompt goes out. The assistant may draft limiter.js from the public file. The reviewer still runs isolation and fail-closed cases that were never pasted into the chat.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Interviewers who want a disposable workspace for assistant iteration can use MonkeyCode's free model access and free server option while keeping the hidden suite off that workspace. The grade still comes from the private tests, not from the tool that produced the first draft.
The packet remains usable without that workspace. A local Node.js install and two test files are enough for scoring.
Limitations and who should not use this packet
This sketch uses a fixed window, not a sliding window and not a token bucket with a refill rate. Fixed windows allow a short burst at the boundary of two adjacent windows. Do not ship this sketch to a billing-critical API without a stronger algorithm and a real shared store with atomic increment.
The memory store is not safe across processes or hosts. Production needs Redis, DynamoDB, or an equivalent that can increment a key without a read-modify-write race. The reference incr on a Map is also racy if callers later wrap it in concurrent check calls against a naive async store.
Skip this packet for intern screens that only need HTTP status literacy. Skip it for roles that will not own multi-tenant abuse controls. Skip it when the interviewer cannot keep a hidden test file off the public gist. Do not treat a green public fixture as evidence that an agent understands tenancy. Do not publish the hidden file in the same repository the candidate clones.
How to run the loop in an interview week
- Send the prompt and
public-fixture.test.jsonly, with the hard constraints copied verbatim. - Allow any assistant if the team already permits tools, including a free remote workspace.
- Collect
limiter.jsplus any extra tests the candidate wrote beside the public fixture. - Run hidden isolation, fail-closed, and clock tests on a clean checkout that the candidate cannot see.
- Score with the table and discuss one failure mode in the debrief, not the README.
The useful signal is whether the submitted key space encodes tenant and route, and whether store errors deny traffic. Everything else is commentary around a module that either isolates customers or does not.
Top comments (0)