{
  "id": "P096",
  "slug": "rate-limiter",
  "key": "P096-rate-limiter",
  "title": "Rate limiter",
  "summary": "One list of request times through three limiters set to the same limit of L per W seconds - a fixed window, a sliding log and a token bucket - showing which requests each lets through and the most each really lets into any W seconds, with the token bucket's exact worst case.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P096 - Rate limiter\n\n\"At most L requests per W seconds\", enforced three ways on one list of\nrequest times: a fixed window, a sliding log and a token bucket. The\nprogram shows which requests each lets through, and the most each really\nlets into any W seconds. It starts with a burst against 10 per 60\nseconds: 10 requests at second 59 and 10 at second 60.\n\n- `main.eml` - the menu, traces, limits and the two tables\n- `limits.eml` - the three limiters, the busiest window, the token\n  bucket's worst case and the random trace\n\nHow each part works:\n\n- A fixed window counts the requests in each window [kW, (k+1)W) and\n  refuses once the count reaches L. As in the corpus case\n  `rate-limit-window-shape`, the count starts again at each boundary. So\n  L requests just before a boundary and L just after all get through: 2L\n  in two seconds.\n- A sliding log keeps the times it let through. It lets a request through\n  when fewer than L of them are in the last W seconds, so no W seconds\n  ever hold more than L.\n- A token bucket starts full with S tokens and gains L tokens every W\n  seconds, never holding more than S. A request takes a token or is\n  refused. Tokens are counted in units of 1/W token, so the refill stays\n  whole. The bucket bounds the burst (S) and the rate over time (L per W),\n  but not each window. Start full, spend each token as it refills, and S\n  plus what refills over the next W - 1 seconds gets through. That is at\n  most S + floor((W - 1) L / W), and less when refilling from one second\n  to the next would overflow the bucket. The program works the number out\n  second by second.\n- The busiest window: two pointers over the times let through give the\n  most in any W seconds in a row, and the earliest second such a window\n  starts.\n- The random trace: a linear congruential generator, each request 0 to 6\n  seconds after the one before.\n\nWhat is checked: menu choices 1 to 5; a trace name; a seed from 0 to\n999999; 2 to 60 request times, whole seconds up to 99999, each no earlier\nthan the one before; a limit of 1 to 100 per 1 to 3600 seconds, and a\nbucket of 1 to the limit. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the burst: the fixed window lets all 20 through within two seconds;\n- each request of the burst;\n- a steady trace, one every 4 seconds: the bucket lets 15 into the first\n  60 seconds;\n- a random trace (seed 7), where the bucket reaches its worst case of 19,\n  and its first 40 requests one by one; then the same trace with a bucket\n  of 3;\n- typed times against 3 per 10 seconds with a bucket of 2, then a burst\n  at those limits.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x; the trace lizard;\n- typed times out of order, with letters, too few, and too large;\n- seeds x and 1000000; limits 0 and 101, windows 0 and 3601, and a bucket\n  bigger than the limit; then cancels.\n\nBuilt on the verified corpus case `rate-limit-window-shape` (a fixed\nwindow lets twice the limit through at a boundary; a sliding window does\nnot; a token bucket bounds the burst, not each window).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P096 rate limiter: one list of requests through a fixed window, a sliding\n# log and a token bucket, each set to \"L per W seconds\" - and the most each\n# actually lets through in any W seconds.\nimport limits\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef seconds(n):\n    if n == 1:\n        return \"1 second\"\n    return str(n) + \" seconds\"\n\ndef lpad(s, n):\n    while len(s) < n:\n        \" \" + s => s\n    return s\n\ndef ask(prompt, low, high):\n    while True:\n        plain(input(prompt + \" (\" + str(low) + \" to \" + str(high) + \")> \")) => a\n        if a == \"\":\n            return -1\n        number(a, 6) => n\n        if n >= low and n <= high:\n            return n\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n\ndef results(times, cfg):\n    [limits.fixed_window(times, cfg[0], cfg[1]), limits.sliding_log(times, cfg[0], cfg[1]), limits.token_bucket(times, cfg[0], cfg[1], cfg[2])] => out\n    return out\n\ndef summary(times, cfg):\n    results(times, cfg) => rs\n    [\"fixed window\", \"sliding log\", \"token bucket\"] => names\n    (str(len(times)) + \" requests from second \" + str(times[0]) + \" to \" + str(times[len(times) - 1]) + \"; the limit is \" + str(cfg[0]) + \" per \" + seconds(cfg[1]) + \" (bucket of \" + str(cfg[2]) + \"):\") ^0\n    \"  limiter        through  refused  most in any \" + str(cfg[1]) + \" s\" => head\n    head ^0\n    for k in [0:2]:\n        0 => n\n        for b in rs[k]:\n            if b:\n                n + 1 => n\n        limits.busiest(times, rs[k], cfg[1]) => m\n        \"\" => note\n        if m[0] > cfg[0]:\n            \"  over the limit, from second \" + str(m[1]) => note\n        (\"  \" + pad(names[k], 15) + lpad(str(n), 7) + lpad(str(len(times) - n), 9) + lpad(str(m[0]), 13) + note) ^0\n    \"A fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\" ^0\n    (\"A token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets \" + str(limits.bucket_most(cfg[0], cfg[1], cfg[2])) + \" through in \" + seconds(cfg[1]) + \" - the most it ever can.\") ^0\n    (\"The sliding log holds every \" + seconds(cfg[1]) + \" to \" + str(cfg[0]) + \".\") ^0\n\ndef steps(times, cfg):\n    results(times, cfg) => rs\n    \"  request  second   fixed  sliding  bucket\" ^0\n    for i in [0:len(times) - 1]:\n        if i < 40:\n            \"\" => row\n            for k in [0:2]:\n                \"  no\" => mark\n                if rs[k][i]:\n                    \" yes\" => mark\n                row + lpad(mark, 8) => row\n            (\"  \" + lpad(str(i + 1), 7) + lpad(str(times[i]), 8) + row) ^0\n    if len(times) > 40:\n        (\"  ... and \" + str(len(times) - 40) + \" more\") ^0\n\ndef burst(cfg):\n    # the limit's worth in the last second of a window and again in the\n    # first second of the next\n    [] => out\n    for i in [1:cfg[0]]:\n        out + [cfg[1] - 1] => out\n    for i in [1:cfg[0]]:\n        out + [cfg[1]] => out\n    return out\n\ndef new_trace(times, cfg):\n    \"Traces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\" ^0\n    plain(input(\"trace (burst, steady, random or own)> \")) => s\n    if s == \"burst\":\n        burst(cfg) => out\n        (str(len(out)) + \" requests: \" + str(cfg[0]) + \" at second \" + str(cfg[1] - 1) + \" and \" + str(cfg[0]) + \" at second \" + str(cfg[1]) + \".\") ^0\n        return out\n    if s == \"steady\":\n        [] => out\n        for i in [0:74]:\n            out + [4 * i] => out\n        \"75 requests, one every 4 seconds.\" ^0\n        return out\n    if s == \"random\":\n        ask(\"seed\", 0, 999999) => seed\n        if seed == -1:\n            \"Cancelled.\" ^0\n            return times\n        limits.lcg_times(60, 3, seed) => out\n        \"60 requests, each 0 to 6 seconds after the one before.\" ^0\n        return out\n    if s == \"own\":\n        plain(input(\"request times in seconds, in order (2 to 60)> \")) => t\n        [] => out\n        \"\" => w\n        True => ok\n        for c in t + \" \":\n            if c == \" \":\n                if w != \"\":\n                    number(w, 5) => v\n                    if v < 0 or (len(out) > 0 and v < out[len(out) - 1]):\n                        False => ok\n                    out + [v] => out\n                \"\" => w\n            else:\n                w + c => w\n        if ok and len(out) >= 2 and len(out) <= 60:\n            (str(len(out)) + \" requests.\") ^0\n            return out\n        \"Type 2 to 60 whole numbers of seconds, each no earlier than the one before.\" ^0\n        return times\n    if s == \"\":\n        \"Cancelled.\" ^0\n    else:\n        \"Type burst, steady, random or own.\" ^0\n    return times\n\ndef settings(cfg):\n    ask(\"requests allowed per window\", 1, 100) => l\n    if l == -1:\n        \"Cancelled.\" ^0\n        return cfg\n    ask(\"window in seconds\", 1, 3600) => w\n    if w == -1:\n        \"Cancelled.\" ^0\n        return cfg\n    ask(\"token bucket size, the largest burst\", 1, l) => b\n    if b == -1:\n        \"Cancelled.\" ^0\n        return cfg\n    (str(l) + \" per \" + seconds(w) + \", bucket of \" + str(b) + \".\") ^0\n    return [l, w, b]\n\n[10, 60, 10] => cfg\nburst(cfg) => times\n\n\"== Rate limiter ==\" ^0\n\"One list of requests, three limiters set to the same limit - and what each really lets through.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) compare  2) request by request  3) new trace  4) limits  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        summary(times, cfg)\n    elif choice == \"2\":\n        steps(times, cfg)\n    elif choice == \"3\":\n        new_trace(times, cfg) => times\n    elif choice == \"4\":\n        settings(cfg) => cfg\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import limits\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef seconds(n):\n    if n == 1:\n        return \"1 second\"\n    return str(n) + \" seconds\"\n\ndef lpad(s, n):\n    while len(s) < n:\n        s = \" \" + s\n    return s\n\ndef ask(prompt, low, high):\n    while True:\n        a = plain(input(prompt + \" (\" + str(low) + \" to \" + str(high) + \")> \"))\n        if a == \"\":\n            return -1\n        n = number(a, 6)\n        if n >= low and n <= high:\n            return n\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n\ndef results(times, cfg):\n    out = [limits.fixed_window(times, cfg[0], cfg[1]), limits.sliding_log(times, cfg[0], cfg[1]), limits.token_bucket(times, cfg[0], cfg[1], cfg[2])]\n    return out\n\ndef summary(times, cfg):\n    rs = results(times, cfg)\n    names = [\"fixed window\", \"sliding log\", \"token bucket\"]\n    print(str(len(times)) + \" requests from second \" + str(times[0]) + \" to \" + str(times[len(times) - 1]) + \"; the limit is \" + str(cfg[0]) + \" per \" + seconds(cfg[1]) + \" (bucket of \" + str(cfg[2]) + \"):\")\n    head = \"  limiter        through  refused  most in any \" + str(cfg[1]) + \" s\"\n    print(head)\n    for k in range(0, 3):\n        n = 0\n        for b in rs[k]:\n            if b:\n                n = n + 1\n        m = limits.busiest(times, rs[k], cfg[1])\n        note = \"\"\n        if m[0] > cfg[0]:\n            note = \"  over the limit, from second \" + str(m[1])\n        print(\"  \" + pad(names[k], 15) + lpad(str(n), 7) + lpad(str(len(times) - n), 9) + lpad(str(m[0]), 13) + note)\n    print(\"A fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\")\n    print(\"A token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets \" + str(limits.bucket_most(cfg[0], cfg[1], cfg[2])) + \" through in \" + seconds(cfg[1]) + \" - the most it ever can.\")\n    print(\"The sliding log holds every \" + seconds(cfg[1]) + \" to \" + str(cfg[0]) + \".\")\n\ndef steps(times, cfg):\n    rs = results(times, cfg)\n    print(\"  request  second   fixed  sliding  bucket\")\n    for i in range(0, len(times)):\n        if i < 40:\n            row = \"\"\n            for k in range(0, 3):\n                mark = \"  no\"\n                if rs[k][i]:\n                    mark = \" yes\"\n                row = row + lpad(mark, 8)\n            print(\"  \" + lpad(str(i + 1), 7) + lpad(str(times[i]), 8) + row)\n    if len(times) > 40:\n        print(\"  ... and \" + str(len(times) - 40) + \" more\")\n\ndef burst(cfg):\n    out = []\n    for i in range(1, cfg[0]+1):\n        out = out + [cfg[1] - 1]\n    for i in range(1, cfg[0]+1):\n        out = out + [cfg[1]]\n    return out\n\ndef new_trace(times, cfg):\n    print(\"Traces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\")\n    s = plain(input(\"trace (burst, steady, random or own)> \"))\n    if s == \"burst\":\n        out = burst(cfg)\n        print(str(len(out)) + \" requests: \" + str(cfg[0]) + \" at second \" + str(cfg[1] - 1) + \" and \" + str(cfg[0]) + \" at second \" + str(cfg[1]) + \".\")\n        return out\n    if s == \"steady\":\n        out = []\n        for i in range(0, 75):\n            out = out + [4 * i]\n        print(\"75 requests, one every 4 seconds.\")\n        return out\n    if s == \"random\":\n        seed = ask(\"seed\", 0, 999999)\n        if seed == -1:\n            print(\"Cancelled.\")\n            return times\n        out = limits.lcg_times(60, 3, seed)\n        print(\"60 requests, each 0 to 6 seconds after the one before.\")\n        return out\n    if s == \"own\":\n        t = plain(input(\"request times in seconds, in order (2 to 60)> \"))\n        out = []\n        w = \"\"\n        ok = True\n        for c in t + \" \":\n            if c == \" \":\n                if w != \"\":\n                    v = number(w, 5)\n                    if v < 0 or len(out) > 0 and v < out[len(out) - 1]:\n                        ok = False\n                    out = out + [v]\n                w = \"\"\n            else:\n                w = w + c\n        if ok and len(out) >= 2 and len(out) <= 60:\n            print(str(len(out)) + \" requests.\")\n            return out\n        print(\"Type 2 to 60 whole numbers of seconds, each no earlier than the one before.\")\n        return times\n    if s == \"\":\n        print(\"Cancelled.\")\n    else:\n        print(\"Type burst, steady, random or own.\")\n    return times\n\ndef settings(cfg):\n    l = ask(\"requests allowed per window\", 1, 100)\n    if l == -1:\n        print(\"Cancelled.\")\n        return cfg\n    w = ask(\"window in seconds\", 1, 3600)\n    if w == -1:\n        print(\"Cancelled.\")\n        return cfg\n    b = ask(\"token bucket size, the largest burst\", 1, l)\n    if b == -1:\n        print(\"Cancelled.\")\n        return cfg\n    print(str(l) + \" per \" + seconds(w) + \", bucket of \" + str(b) + \".\")\n    return [l, w, b]\n\ncfg = [10, 60, 10]\ntimes = burst(cfg)\nprint(\"== Rate limiter ==\")\nprint(\"One list of requests, three limiters set to the same limit - and what each really lets through.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) compare  2) request by request  3) new trace  4) limits  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        summary(times, cfg)\n    elif choice == \"2\":\n        steps(times, cfg)\n    elif choice == \"3\":\n        times = new_trace(times, cfg)\n    elif choice == \"4\":\n        cfg = settings(cfg)\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "limits.eml",
      "eml": "# P096 rate limiter - \"at most L requests per W seconds\", enforced three\n# ways on one list of request times (whole seconds, in order), as in the\n# corpus case rate-limit-window-shape: a fixed window, a sliding log, and a\n# token bucket. Each returns, for every request, True (let through) or\n# False (refused).\n\ndef quotient(a, b):\n    # a // b for a >= 0, b > 0; the numbers here are small, so the float\n    # division is exact.\n    return int((a - a % b) / b)\n\ndef fixed_window(times, limit, w):\n    # Count requests per window [kW, (k+1)W) and refuse once the count\n    # reaches the limit; the count starts again at each boundary.\n    [] => out\n    -1 => current\n    0 => count\n    for t in times:\n        quotient(t, w) => k\n        if k != current:\n            k => current\n            0 => count\n        if count < limit:\n            count + 1 => count\n            out + [True] => out\n        else:\n            out + [False] => out\n    return out\n\ndef sliding_log(times, limit, w):\n    # Keep the times of the requests let through; let a request through when\n    # fewer than `limit` of them are in the last W seconds (t - W, t].\n    [] => out\n    [] => kept\n    0 => first\n    for t in times:\n        while first < len(kept) and kept[first] <= t - w:\n            first + 1 => first\n        if len(kept) - first < limit:\n            kept + [t] => kept\n            out + [True] => out\n        else:\n            out + [False] => out\n    return out\n\ndef token_bucket(times, limit, w, size):\n    # A bucket of `size` tokens, starting full, refilled at `limit` tokens\n    # per W seconds; a request takes a token or is refused. Tokens are kept\n    # in units of 1/W token, so the refill (elapsed x limit) stays whole.\n    size * w => tokens\n    size * w => full\n    -1 => last\n    [] => out\n    for t in times:\n        if last >= 0:\n            tokens + (t - last) * limit => tokens\n            if tokens > full:\n                full => tokens\n        t => last\n        if tokens >= w:\n            tokens - w => tokens\n            out + [True] => out\n        else:\n            out + [False] => out\n    return out\n\ndef bucket_most(limit, w, size):\n    # The most a token bucket ever lets through in W seconds: start full,\n    # spend every whole token at once, then each one as it refills - a\n    # request every second. The bucket never holds more than `size`, so\n    # refill that would overflow it is lost.\n    size * w => tokens\n    0 => most\n    for t in [0:w - 1]:\n        if t > 0:\n            tokens + limit => tokens\n            if tokens > size * w:\n                size * w => tokens\n        quotient(tokens, w) => k\n        most + k => most\n        tokens - k * w => tokens\n    return most\n\ndef busiest(times, through, w):\n    # The most requests let through in any W seconds in a row, and when\n    # that window starts: the real limit a client meets.\n    [] => ok\n    for i in [0:len(times) - 1]:\n        if through[i]:\n            ok + [times[i]] => ok\n    0 => most\n    -1 => at\n    0 => j\n    for i in [0:len(ok) - 1]:\n        while ok[i] - ok[j] >= w:\n            j + 1 => j\n        if i - j + 1 > most:\n            i - j + 1 => most\n            ok[j] => at\n    return [most, at]\n\ndef lcg_times(n, gap, seed):\n    # n request times, each a random 0 to 2 x gap seconds after the one\n    # before (a linear congruential generator, top 16 bits).\n    [] => out\n    seed => x\n    0 => t\n    for i in [1:n]:\n        (1664525 * x + 1013904223) % 4294967296 => x\n        t + quotient(x, 65536) % (2 * gap + 1) => t\n        out + [t] => out\n    return out\n",
      "python": "def quotient(a, b):\n    return int((a - a % b) / b)\n\ndef fixed_window(times, limit, w):\n    out = []\n    current = -1\n    count = 0\n    for t in times:\n        k = quotient(t, w)\n        if k != current:\n            current = k\n            count = 0\n        if count < limit:\n            count = count + 1\n            out = out + [True]\n        else:\n            out = out + [False]\n    return out\n\ndef sliding_log(times, limit, w):\n    out = []\n    kept = []\n    first = 0\n    for t in times:\n        while first < len(kept) and kept[first] <= t - w:\n            first = first + 1\n        if len(kept) - first < limit:\n            kept = kept + [t]\n            out = out + [True]\n        else:\n            out = out + [False]\n    return out\n\ndef token_bucket(times, limit, w, size):\n    tokens = size * w\n    full = size * w\n    last = -1\n    out = []\n    for t in times:\n        if last >= 0:\n            tokens = tokens + (t - last) * limit\n            if tokens > full:\n                tokens = full\n        last = t\n        if tokens >= w:\n            tokens = tokens - w\n            out = out + [True]\n        else:\n            out = out + [False]\n    return out\n\ndef bucket_most(limit, w, size):\n    tokens = size * w\n    most = 0\n    for t in range(0, w):\n        if t > 0:\n            tokens = tokens + limit\n            if tokens > size * w:\n                tokens = size * w\n        k = quotient(tokens, w)\n        most = most + k\n        tokens = tokens - k * w\n    return most\n\ndef busiest(times, through, w):\n    ok = []\n    for i in range(0, len(times)):\n        if through[i]:\n            ok = ok + [times[i]]\n    most = 0\n    at = -1\n    j = 0\n    for i in range(0, len(ok)):\n        while ok[i] - ok[j] >= w:\n            j = j + 1\n        if i - j + 1 > most:\n            most = i - j + 1\n            at = ok[j]\n    return [most, at]\n\ndef lcg_times(n, gap, seed):\n    out = []\n    x = seed\n    t = 0\n    for i in range(1, n+1):\n        x = (1664525 * x + 1013904223) % 4294967296\n        t = t + quotient(x, 65536) % (2 * gap + 1)\n        out = out + [t]\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n3\nlizard\n3\nown\n5 3\n3\nown\nabc 4\n3\nown\n7\n3\nown\n1 123456\n3\nrandom\nx\n1000000\n\n4\n0\n101\n5\n0\n3601\n60\n6\n\n3\n\n2\n5\n",
      "screen": "== Rate limiter ==\nOne list of requests, three limiters set to the same limit - and what each really lets through.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> lizard\nType burst, steady, random or own.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> own\nrequest times in seconds, in order (2 to 60)> 5 3\nType 2 to 60 whole numbers of seconds, each no earlier than the one before.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> own\nrequest times in seconds, in order (2 to 60)> abc 4\nType 2 to 60 whole numbers of seconds, each no earlier than the one before.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> own\nrequest times in seconds, in order (2 to 60)> 7\nType 2 to 60 whole numbers of seconds, each no earlier than the one before.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> own\nrequest times in seconds, in order (2 to 60)> 1 123456\nType 2 to 60 whole numbers of seconds, each no earlier than the one before.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> random\nseed (0 to 999999)> x\nType a whole number from 0 to 999999.\nseed (0 to 999999)> 1000000\nType a whole number from 0 to 999999.\nseed (0 to 999999)> \nCancelled.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 4\nrequests allowed per window (1 to 100)> 0\nType a whole number from 1 to 100.\nrequests allowed per window (1 to 100)> 101\nType a whole number from 1 to 100.\nrequests allowed per window (1 to 100)> 5\nwindow in seconds (1 to 3600)> 0\nType a whole number from 1 to 3600.\nwindow in seconds (1 to 3600)> 3601\nType a whole number from 1 to 3600.\nwindow in seconds (1 to 3600)> 60\ntoken bucket size, the largest burst (1 to 5)> 6\nType a whole number from 1 to 5.\ntoken bucket size, the largest burst (1 to 5)> \nCancelled.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> \nCancelled.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 2\n  request  second   fixed  sliding  bucket\n        1      59     yes     yes     yes\n        2      59     yes     yes     yes\n        3      59     yes     yes     yes\n        4      59     yes     yes     yes\n        5      59     yes     yes     yes\n        6      59     yes     yes     yes\n        7      59     yes     yes     yes\n        8      59     yes     yes     yes\n        9      59     yes     yes     yes\n       10      59     yes     yes     yes\n       11      60     yes      no      no\n       12      60     yes      no      no\n       13      60     yes      no      no\n       14      60     yes      no      no\n       15      60     yes      no      no\n       16      60     yes      no      no\n       17      60     yes      no      no\n       18      60     yes      no      no\n       19      60     yes      no      no\n       20      60     yes      no      no\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\nsteady\n1\n3\nrandom\n7\n1\n2\n4\n10\n60\n3\n1\n3\nown\n0 0 0 5 5 5 9 10 11 12 19 20\n4\n3\n10\n2\n1\n2\n3\nburst\n1\n5\n",
      "screen": "== Rate limiter ==\nOne list of requests, three limiters set to the same limit - and what each really lets through.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 1\n20 requests from second 59 to 60; the limit is 10 per 60 seconds (bucket of 10):\n  limiter        through  refused  most in any 60 s\n  fixed window        20        0           20  over the limit, from second 59\n  sliding log         10       10           10\n  token bucket        10       10           10\nA fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\nA token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets 19 through in 60 seconds - the most it ever can.\nThe sliding log holds every 60 seconds to 10.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 2\n  request  second   fixed  sliding  bucket\n        1      59     yes     yes     yes\n        2      59     yes     yes     yes\n        3      59     yes     yes     yes\n        4      59     yes     yes     yes\n        5      59     yes     yes     yes\n        6      59     yes     yes     yes\n        7      59     yes     yes     yes\n        8      59     yes     yes     yes\n        9      59     yes     yes     yes\n       10      59     yes     yes     yes\n       11      60     yes      no      no\n       12      60     yes      no      no\n       13      60     yes      no      no\n       14      60     yes      no      no\n       15      60     yes      no      no\n       16      60     yes      no      no\n       17      60     yes      no      no\n       18      60     yes      no      no\n       19      60     yes      no      no\n       20      60     yes      no      no\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> steady\n75 requests, one every 4 seconds.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 1\n75 requests from second 0 to 296; the limit is 10 per 60 seconds (bucket of 10):\n  limiter        through  refused  most in any 60 s\n  fixed window        50       25           10\n  sliding log         50       25           10\n  token bucket        59       16           15  over the limit, from second 0\nA fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\nA token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets 19 through in 60 seconds - the most it ever can.\nThe sliding log holds every 60 seconds to 10.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> random\nseed (0 to 999999)> 7\n60 requests, each 0 to 6 seconds after the one before.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 1\n60 requests from second 3 to 181; the limit is 10 per 60 seconds (bucket of 10):\n  limiter        through  refused  most in any 60 s\n  fixed window        32       28           13  over the limit, from second 5\n  sliding log         30       30           10\n  token bucket        39       21           19  over the limit, from second 3\nA fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\nA token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets 19 through in 60 seconds - the most it ever can.\nThe sliding log holds every 60 seconds to 10.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 2\n  request  second   fixed  sliding  bucket\n        1       3     yes     yes     yes\n        2       5     yes     yes     yes\n        3       7     yes     yes     yes\n        4      11     yes     yes     yes\n        5      17     yes     yes     yes\n        6      17     yes     yes     yes\n        7      21     yes     yes     yes\n        8      22     yes     yes     yes\n        9      28     yes     yes     yes\n       10      34     yes     yes     yes\n       11      38      no      no     yes\n       12      43      no      no     yes\n       13      45      no      no     yes\n       14      45      no      no     yes\n       15      48      no      no     yes\n       16      50      no      no     yes\n       17      55      no      no     yes\n       18      56      no      no     yes\n       19      61     yes      no     yes\n       20      61     yes      no      no\n       21      64     yes     yes     yes\n       22      64     yes      no      no\n       23      69     yes     yes     yes\n       24      73     yes     yes      no\n       25      73     yes     yes      no\n       26      78     yes     yes     yes\n       27      81     yes     yes     yes\n       28      84     yes     yes      no\n       29      89      no     yes     yes\n       30      94      no     yes     yes\n       31      95      no     yes      no\n       32      96      no      no      no\n       33      99      no      no     yes\n       34     102      no      no      no\n       35     105      no      no     yes\n       36     108      no      no      no\n       37     111      no      no     yes\n       38     116      no      no      no\n       39     121     yes      no     yes\n       40     124     yes     yes     yes\n  ... and 20 more\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 4\nrequests allowed per window (1 to 100)> 10\nwindow in seconds (1 to 3600)> 60\ntoken bucket size, the largest burst (1 to 10)> 3\n10 per 60 seconds, bucket of 3.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 1\n60 requests from second 3 to 181; the limit is 10 per 60 seconds (bucket of 3):\n  limiter        through  refused  most in any 60 s\n  fixed window        32       28           13  over the limit, from second 5\n  sliding log         30       30           10\n  token bucket        32       28           12  over the limit, from second 3\nA fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\nA token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets 12 through in 60 seconds - the most it ever can.\nThe sliding log holds every 60 seconds to 10.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> own\nrequest times in seconds, in order (2 to 60)> 0 0 0 5 5 5 9 10 11 12 19 20\n12 requests.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 4\nrequests allowed per window (1 to 100)> 3\nwindow in seconds (1 to 3600)> 10\ntoken bucket size, the largest burst (1 to 3)> 2\n3 per 10 seconds, bucket of 2.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 1\n12 requests from second 0 to 20; the limit is 3 per 10 seconds (bucket of 2):\n  limiter        through  refused  most in any 10 s\n  fixed window         7        5            3\n  sliding log          7        5            3\n  token bucket         7        5            4  over the limit, from second 0\nA fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\nA token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets 4 through in 10 seconds - the most it ever can.\nThe sliding log holds every 10 seconds to 3.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 2\n  request  second   fixed  sliding  bucket\n        1       0     yes     yes     yes\n        2       0     yes     yes     yes\n        3       0     yes     yes      no\n        4       5      no      no     yes\n        5       5      no      no      no\n        6       5      no      no      no\n        7       9      no      no     yes\n        8      10     yes     yes     yes\n        9      11     yes     yes      no\n       10      12     yes     yes      no\n       11      19      no      no     yes\n       12      20     yes     yes     yes\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 3\nTraces: burst (the limit's worth just before a boundary and just after), steady, random, or your own times.\ntrace (burst, steady, random or own)> burst\n6 requests: 3 at second 9 and 3 at second 10.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 1\n6 requests from second 9 to 10; the limit is 3 per 10 seconds (bucket of 2):\n  limiter        through  refused  most in any 10 s\n  fixed window         6        0            6  over the limit, from second 9\n  sliding log          3        3            3\n  token bucket         2        4            2\nA fixed window starts counting again at each boundary, so a burst just before it and another just after both get through.\nA token bucket bounds the burst and the rate over time, not each window: full, then spending each token as it refills, it lets 4 through in 10 seconds - the most it ever can.\nThe sliding log holds every 10 seconds to 3.\n\n1) compare  2) request by request  3) new trace  4) limits  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "rate-limit-window-shape",
      "caseId": "257-rate-limit-window-shape",
      "title": "Rate limit window shape — three limiters, three actual limits"
    }
  ],
  "updated": "2026-10-15"
}
