Map Tile Burst Allowance

MediumdesignAlso known as Token Bucket Rate Limiter

A map company sells tile access to app developers. When a user pans the map, the app needs a burst of tiles at once, then nothing for a while. A fixed cap per minute would either block the burst or allow too much in total, so each API key gets a token bucket: it holds up to capacity tokens, a request spends tokens, and one new token drips in every intervalMs milliseconds. A full bucket allows a burst; the drip sets the long-run rate.

This is the token bucket from Rate Limiting, the usual choice for API gateways (see Design a Rate Limiter). No timer refills it: each request works out how many tokens arrived since the last one, which is where most bugs hide.

Your task

Write a class TokenBucket:

  • constructor(capacity, intervalMs): the bucket starts full at time 0.
  • take(t, cost): a request at time t (milliseconds) that needs cost tokens. If the bucket holds at least cost tokens, remove them and return true. Otherwise return false and remove nothing.
  • available(t): return how many tokens the bucket holds at time t, without changing anything else.

Drips happen at fixed times: intervalMs, 2 × intervalMs, 3 × intervalMs, and so on. Each drip adds one token, unless the bucket is already full, in which case that token is lost. A drip at exactly time t counts before the request at t.

class TokenBucket

Examples

Example 1
Input
new TokenBucket(3, 1000)
.take(0, 1)
.take(100, 1)
.take(200, 1)
.take(300, 1)
.take(1000, 1)
.take(1500, 1)
.available(2000)
Output
[null, true, true, true, false, true, false, 1]
Why

The bucket starts with 3 tokens, so the burst at 0, 100 and 200 ms is allowed and empties it. At 300 ms nothing has dripped yet: rejected. The drip at 1,000 ms adds one token, which the request at 1,000 spends. At 1,500 the bucket is empty again. The drip at 2,000 leaves 1 token.

Example 2
Input
new TokenBucket(2, 1000)
.take(0, 2)
.take(600, 1)
.take(1200, 1)
.take(1900, 1)
.take(2000, 1)
Output
[null, true, false, true, false, true]
Why

A cost-2 request empties the bucket at 0. At 600 ms no drip has happened. At 1,200 ms the drip at 1,000 has added a token, so the request is allowed. A common bug measures 600 ms "since the last request" at both 600 and 1,200, never reaches 1,000, and wrongly rejects this one. The drip at 2,000 pays for the last request.

Constraints

  • 1 ≤ capacity ≤ 1,000,000
  • 1 ≤ intervalMs ≤ 1,000,000
  • 1 ≤ cost ≤ 1,000,000 (a cost above capacity can never succeed)
  • 0 ≤ t ≤ 1,000,000,000,000, and t never decreases from one call to the next
  • At most 2,000 calls per test
Start with brute force

A timer that adds one token every interval works on paper, but a gateway with a million API keys cannot run a million timers. Simulating every drip one by one inside each call is also too slow: with a 1 ms interval and calls a trillion milliseconds apart, that is a trillion steps.

Counting drips with one division gives the same result in O(1), and the bucket is just two numbers per key, small enough to keep in a shared in-memory store.

Hints

0 of 4 shown

Walkthrough and solution

Unlocks when you pass every test. Stuck for a while? Looking is fine too, as long as you then close it and write the code yourself.

Ctrl+Enter runs

Run your code to see results here. Run checks the examples. Submit checks every test, including hidden edge cases.

Write code