Kata: throttle (leading + trailing)
The window is a timer, not a timestamp: only a timer can run the last call, and that run opens the next window.
Predict first
Before you read: predict
ExamHall's proctor console reports a candidate's webcam motion to the proctor's feed. A pairing-round draft created the reporter as throttle(reportMotion, 100), with this kata answer:
The camera sends motion events ev1 to ev5 at t = 0, 40, 80, 150 and 190 ms, the last one as the candidate leaves the frame, and nothing more before t = 1000 ms.
Which calls of reportMotion happen?
export function throttle(fn, wait) { let last = -Infinity; return function (...args) { const now = Date.now(); if (now - last >= wait) { last = now; fn.apply(this, args); } };}The 30-second version
A kata throttle keeps a timer for the open window, plus the latest saved call and its this. A call with no window open runs at once and opens one; calls inside it overwrite the saved call. When the window ends, the saved call runs and opens the next window. A Date.now() check alone never runs a refused call later.
The window is a timer, not a timestamp: only a timer can run the last call, and that run opens the next window.
How it works
Step 1 of 11
cancel, flush, leading or trailing is wanted. Then trace three calls inside one window on paper before typing.Analogy: A fire lookout's radio rule (wildfire watch)
A lookout may radio the valley at most once per turn of a ten-minute sand-glass. She keeps one chalk slate for sightings.
| At the lookout tower | In the kata |
|---|---|
| a sighting on a quiet channel: radio it at once, turn the glass | run now, set the timer (step 3) |
| sightings while the glass runs: chalk each on the slate, rubbing out the last | overwrite lastArgs (step 4) |
| the glass runs out with something chalked: radio it, wipe the slate, turn the glass again | the saved call runs and opens the next window (step 5) |
| the glass runs out on an empty slate: lay it down | timer = null, nothing saved (step 5) |
| a lookout who glances at the clock before speaking and keeps no slate | a Date.now() check alone (step 6) |
| "stand down": lay the glass flat, wipe the slate; "report now": radio the slate, turn the glass | cancel(), flush() (step 8) |
In real terms: a throttle is one timer that marks the open window, one saved call, and a rule that every run starts a window.
Breaks down when: a lookout judges whether a sighting is worth a call; the code never judges, it keeps the latest call whatever it holds. The one-call-per-glass rule is this scene's own, not a fire-service procedure.
Contrasting case: a lookout who must stay silent until ten quiet minutes pass after the last sighting. Every sighting turns the glass back: that is a debounce (kata-debounce), not a throttle.
Examples, run for real
4.1 ExamHall EXH-2770: the test list catches the two common mutants
The proctor console throttles webcam motion to one report per 100 ms. Its first reporter used a Date.now() check alone (EXH-2770): a candidate's last movement before leaving the frame never reached the feed. MR !3070 replaced it with the reference answer and added the test list. The list runs on the reference and on two mutants: M1 is the Date.now() check alone, and M2 saves only a window's first refused call.
4.1 reference.mjs 16 of 16 tests (8 core, 8 ladder) pass
4.1 M1, Date.now() check alone 4 of 8 core tests pass · red: "calls inside the window make one run at its end, with the last call's arguments", "fn gets the caller's this on both runs", "the trailing run opens the next window", "a steady stream runs once per window"
4.1 M2, first refused call kept 7 of 8 core tests pass · red: "calls inside the window make one run at its end, with the last call's arguments"
4.1 EXH-2770 motion at 0/30/60 ms, then out of frame: M1 reports 0 ms "x=40"; MR !3070 (reference) reports 0 ms "x=40" · 100 ms "x=95"
M1 has nothing that runs a refused call (step 6), so every test that expects a run at a window's end goes red. M2 runs at the right times with the wrong call (step 4). On the incident's three events, only the reference reports the last position.
4.2 Payments ledger: flush and cancel on the balance redraw
A ledger page redraws the running balance after each posting through throttle(redraw, 500). Save calls flush(), and closing the statement calls cancel():
4.2 balance redraw: e1 0, e2 150, e3 300, Save 350, e4 1000, e5 1100, close 1200 -> redraws 0 ms "e1" · 350 ms "e3" · 1000 ms "e4"
Save ran the waiting e3 at once and opened a window that ended at 850 (step 8), so e4 found no window open and ran at once (step 3). Closing the statement dropped e5, saved inside the next window.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A payments ledger sends each new posting's running total to the audit feed through audit = throttle(sendTotal, 1000). Its kata answer:
Postings e1 to e4 arrive at t = 0, 300, 1200 and 1500 ms, and nothing more before t = 5000 ms.
How many calls of sendTotal happen, and when is the last one?
export function throttle(fn, wait) { let timer = null; let lastArgs = null; let lastThis = null; function onWindowEnd() { timer = null; if (lastArgs) { const args = lastArgs; const self = lastThis; lastArgs = lastThis = null; fn.apply(self, args); } } return function (...args) { if (timer === null) { fn.apply(this, args); timer = setTimeout(onWindowEnd, wait); } else { lastArgs = args; lastThis = this; } };}Question 2 of 8
ExamHall's proctor console shares one proctor's pointer with a second proctor through share = throttle(sendPointer, 100). A candidate's kata answer uses Date.now() and one timer, and its callback is still a gap:
The answer has no cancel yet, so the 6 core tests of the team's list that do not call cancel are run.
Which callback passes all 6?
export function throttle(fn, wait) { let last = -Infinity; let timer = null; let lastArgs = null; let lastThis = null; return function throttled(...args) { const now = Date.now(); const remaining = wait - (now - last); if (remaining <= 0) { last = now; fn.apply(this, args); return; } lastArgs = args; lastThis = this; if (timer === null) { timer = setTimeout(/* the callback */, remaining); } };}Question 3 of 8
A chat app shows the other person a live preview of the message being typed, through preview = throttle(sendDraft, 1000), and Enter calls preview.flush(). Its kata answer:
A user types o at t = 0 and ok at 400 ms, presses Enter at 600 ms, then types s at 800 ms and so at 1100 ms, and nothing more before t = 5000 ms.
Which calls of sendDraft happen?
export function throttle(fn, wait) { let timer = null; let lastArgs = null; let lastThis = null; function onWindowEnd() { timer = null; if (lastArgs) { const args = lastArgs; const self = lastThis; lastArgs = lastThis = null; fn.apply(self, args); timer = setTimeout(onWindowEnd, wait); } } function throttled(...args) { if (timer !== null) { lastArgs = args; lastThis = this; return; } fn.apply(this, args); timer = setTimeout(onWindowEnd, wait); } throttled.flush = () => { if (lastArgs === null) return; clearTimeout(timer); timer = null; const args = lastArgs; const self = lastThis; lastArgs = lastThis = null; fn.apply(self, args); }; return throttled;}Question 4 of 8
A live stream's reaction button sends the viewer's latest reaction through react = throttle(sendReaction, 1000, { leading: false }), so that a burst of taps sends one reaction, later. Its kata answer:
The viewer taps clap at t = 0, heart at 300 ms and wow at 1500 ms, and nothing more before t = 5000 ms.
Which calls of sendReaction happen?
export function throttle(fn, wait, { leading = true, trailing = true } = {}) { let timer = null; let lastArgs = null; let lastThis = null; function onWindowEnd() { timer = null; if (trailing && lastArgs) { const args = lastArgs; const self = lastThis; lastArgs = lastThis = null; fn.apply(self, args); timer = setTimeout(onWindowEnd, wait); } } return function throttled(...args) { if (timer !== null) { lastArgs = args; lastThis = this; return; } if (leading) { fn.apply(this, args); timer = setTimeout(onWindowEnd, wait); } else { lastArgs = args; lastThis = this; } };}Question 5 of 8
the interviewer says: "Implement throttle(fn, wait) for the proctor console's webcam motion: report at most once per 100 ms, and never drop the last movement. Then add cancel()." You have 15 minutes and a plain editor.
Before you type: say your test list out loud.
Name at least six tests, each with its calls and what it expects.
Question 6 of 8
the interviewer says: "Implement throttle(fn, wait) for the proctor console's webcam motion: report at most once per 100 ms, and never drop the last movement. Then add cancel()." You have 15 minutes and a plain editor.
Now write it, in 15 minutes, from a blank file that exports throttle.
Question 7 of 8
A shop's product list checks whether the shopper has scrolled near the bottom through checkScroll = throttle(loadMoreIfNear, 1000). Its kata answer:
The shopper scrolls at t = 0, 600, 900 and 1700 ms, and nothing more before t = 9000 ms. The list is checked at 0, 1600 and 2700 ms.
Which second argument to setTimeout, in place of wait, makes the checks run at 0, 1000 and 2000 ms and at no other time?
export function throttle(fn, wait) { let last = -Infinity; let prevCall = -Infinity; let timer = null; let lastArgs = null; let lastThis = null; function onTimer() { timer = null; last = Date.now(); fn.apply(lastThis, lastArgs); } return function throttled(...args) { const now = Date.now(); const elapsed = now - last; if (elapsed >= wait) { clearTimeout(timer); timer = null; last = now; fn.apply(this, args); } else { lastArgs = args; lastThis = this; if (timer === null) timer = setTimeout(onTimer, wait); } prevCall = now; };}Question 8 of 8
A box office's seat picker sends the seat under the pointer to the hold service through pick = throttle(sendSeat, 500), and Esc calls pick.cancel(). Its kata answer, apart from the body of cancel:
The team runs its 8 core tests; 2 of them call cancel().
Which body passes all 8?
export function throttle(fn, wait) { let last = -Infinity; let timer = null; let lastArgs = null; let lastThis = null; function onTimer() { timer = null; last = Date.now(); fn.apply(lastThis, lastArgs); } function throttled(...args) { const now = Date.now(); const remaining = wait - (now - last); if (remaining <= 0) { clearTimeout(timer); timer = null; last = now; fn.apply(this, args); return; } lastArgs = args; lastThis = this; if (timer === null) timer = setTimeout(onTimer, remaining); } throttled.cancel = function () { /* the body */ }; return throttled;}Go deeper
Pro tips
- Trace calls at 0, 30 and 60 ms on paper before coding. When: the first minute of the kata, after the clarifying questions (step 1). A correct leading + trailing throttle with wait 100 runs at 0 and at 100 with the 60 ms call's arguments (test T2). If your trace differs, your model is wrong. Verify (RAN,
tips.mjs): (The curriculum row's tip, [RAN].)tip 1 calls a, b, c at 0, 30, 60 ms, wait 100: reference runs 0 ms "a" · 100 ms "c"; M1 runs 0 ms "a" - Run your own answer against the list before you call it done:
KATA_IMPL=./my-throttle.mjs node --test kata-core.test.mjs. When: practising the kata at home, against the same tests the reference passes (step 11). Verify (RAN,tips.mjs):tip 2 KATA_IMPL=./reference-core.mjs: exit 0 · tests 8 · pass 8 · fail 0 tip 2 KATA_IMPL=<temp>/m1-throttle.mjs (Date.now() check alone): exit 1 · tests 8 · pass 4 · fail 4 · red: "calls inside the window make one run at its end, with the last call's arguments", "fn gets the caller's this on both runs", "the trailing run opens the next window", "a steady stream runs once per window"
Source
Vendor and guide pages change, so their claims are labelled as of 2026-10-04; every behaviour this lesson grades is executed on Node 24.19.0.
- Front End Interview Handbook, JavaScript coding (the curriculum row's first source), https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (
feih-javascript-coding.txt). It lists throttle among the Lodash functions asked in interviews, says candidates take "around 10-15 minutes for a basic question" (the time box, step 1), and advises writing test cases (step 11). As of 2026-10-04. - Lodash 4.17.15 documentation,
_.throttle(the row's second source), https://lodash.com/docs/4.17.15#throttle. lodash.com refused connections on 2026-10-04 (curl exit 28; the R2 fix-brief writer's retry at 05:58:32Z also gavecurl: (28)), so the page was read from the unmodified Internet Archive capture (id_, Memento-Datetime 2025-12-05T10:14:29Z) of that URL, the treatment_fix-brief-r1.mdruling 1 accepts for a named primary page (lodash-throttle.txt, header linesViaandMemento-Datetime). It saysfuncruns "at most once per every wait milliseconds", "is invoked with the last arguments provided", and namescancelandflush(steps 4, 8). This lesson's reference is its own code, not Lodash's. - CSS-Tricks, Debouncing and Throttling Explained Through Examples (the row's third source), https://css-tricks.com/debouncing-throttling-explained-examples/ (
css-tricks-debounce-throttle.txt). It advisesrequestAnimationFramewhen a function is "painting" or animating, and debounce or throttle for Ajax requests and class changes (step 10). As of 2026-10-04. - MDN, Window: requestAnimationFrame() method, https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame (
mdn-requestanimationframe.txt): it "requests the browser to call a user-supplied callback function before the next repaint", and the frequency of calls "will generally match the display refresh rate" (step 10). As of 2026-10-04. - Node.js v24 documentation, Test runner,
Class: MockTimers, https://nodejs.org/docs/latest-v24.x/api/test.html (node-test-mocktimers.txt):mock.timers.enable()withDate, andtick()(step 11). As of 2026-10-04.
Related lessons
Checked against the tools and docs as of 2026-10-04.