{
  "path": "/3lzrjubkqck2k",
  "site": "at://did:plc:57od6g2ic3e3b3kauctjmo3k/site.standard.publication/3lwagtcm36s2d",
  "$type": "site.standard.document",
  "title": "Sliding Window Rate Limiting with Deno KV",
  "content": {
    "$type": "pub.leaflet.content",
    "pages": [
      {
        "$type": "pub.leaflet.pages.linearDocument",
        "blocks": [
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [
                {
                  "index": {
                    "byteEnd": 57,
                    "byteStart": 50
                  },
                  "features": [
                    {
                      "uri": "https://omg.lol",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 200,
                    "byteStart": 181
                  },
                  "features": [
                    {
                      "uri": "https://cistern.app",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                }
              ],
              "plaintext": "The following post was originally published on my omg.lol Weblog on February 7th, 2024. The app that it refers to as Fissure is now known as Cistern, and a WIP version is usable at https://cistern.app. I've corrected a couple typos and tweaked the code example, which was bugged 🤪"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": ""
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "I'm building out an ingestion API for Obsidian called Fissure, and since I want to offer it for free to a small extent, I was looking into adding a rate limiter to reduce abuse. I'm deploying on Deno Deploy, and my storage backend is Deno KV, so I felt that a sliding window rate limiter was a good choice."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "There are more detailed articles on the subject, but to my understanding the algorithm works something like this:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.unorderedList",
              "children": [
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [],
                    "plaintext": "Define a fixed interval of time as the window period—e.g. 1 hour, 1 day"
                  },
                  "children": []
                },
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [],
                    "plaintext": "When a request comes in, get the number of requests made in the current period and the previous period"
                  },
                  "children": []
                }
              ]
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [],
              "plaintext": "If the time is 3:45pm and your window period is 1 hour, get the number of requests made between 2–3pm and 3–4pm."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.unorderedList",
              "children": [
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [
                      {
                        "index": {
                          "byteEnd": 48,
                          "byteStart": 40
                        },
                        "features": [
                          {
                            "$type": "pub.leaflet.richtext.facet#italic"
                          }
                        ]
                      }
                    ],
                    "plaintext": "Calculate the percentage overlap of the previous period with the current sliding window"
                  },
                  "children": []
                }
              ]
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [],
              "plaintext": "If, again, the time is 3:45pm and your window period is 1 hour, your current window is 2:45pm–3:45pm—from an hour ago until the current moment. Therefore, the percentage overlap of the previous period with the current window is 0.25, or 25%."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.unorderedList",
              "children": [
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [
                      {
                        "index": {
                          "byteEnd": 85,
                          "byteStart": 84
                        },
                        "features": [
                          {
                            "$type": "pub.leaflet.richtext.facet#code"
                          }
                        ]
                      }
                    ],
                    "plaintext": "With all that information, use the following equation to get the number of requests n with the following values:"
                  },
                  "children": []
                },
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [
                      {
                        "index": {
                          "byteEnd": 1,
                          "byteStart": 0
                        },
                        "features": [
                          {
                            "$type": "pub.leaflet.richtext.facet#code"
                          }
                        ]
                      }
                    ],
                    "plaintext": "x is the number of requests made in current period"
                  },
                  "children": []
                },
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [
                      {
                        "index": {
                          "byteEnd": 1,
                          "byteStart": 0
                        },
                        "features": [
                          {
                            "$type": "pub.leaflet.richtext.facet#code"
                          }
                        ]
                      },
                      {
                        "index": {
                          "byteEnd": 43,
                          "byteStart": 35
                        },
                        "features": [
                          {
                            "$type": "pub.leaflet.richtext.facet#italic"
                          }
                        ]
                      }
                    ],
                    "plaintext": "y is the number of requests in the previous period"
                  },
                  "children": []
                },
                {
                  "$type": "pub.leaflet.blocks.unorderedList#listItem",
                  "content": {
                    "$type": "pub.leaflet.blocks.text",
                    "facets": [
                      {
                        "index": {
                          "byteEnd": 1,
                          "byteStart": 0
                        },
                        "features": [
                          {
                            "$type": "pub.leaflet.richtext.facet#code"
                          }
                        ]
                      }
                    ],
                    "plaintext": "z is the percentage overlap of the previous period with the current window"
                  },
                  "children": []
                }
              ]
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "plaintext",
              "plaintext": "n = x + y * z",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 4,
                    "byteStart": 3
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "If n is greater than or equal to your maximum number of periodic requests, drop it. Here's roughly the implementation that I settled on:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "import { eachHourOfInterval, subHours } from 'npm:date-fns'\n\nconst kv = await Deno.openKv()\nconst MAXIMUM_HOURLY_REQUESTS = 50\nconst HOUR_IN_MILLISECONDS = 3_600_000\n\nasync function shouldDropRequest(userId: string): Promise<boolean> {\n  const windowEnd = new Date();\n  const windowStart = subHours(windowEnd, 1);\n  const [lastHour, currentHour] = eachHourOfInterval({\n    start: windowStart,\n    end: windowEnd,\n  });\n  const [lastPeriod, currentPeriod] = await kv.getMany<\n    [Deno.KvU64, Deno.KvU64]\n  >([\n    [\"user\", userId, \"period\", lastHour.getTime().toString()],\n    [\"user\", userId, \"period\", currentHour.getTime().toString()],\n  ]);\n\n  const currentRequests = Number(currentPeriod.value?.value ?? 0n);\n  const previousRequests = Number(lastPeriod.value?.value ?? 0n);\n  const previousWeight = (currentHour.getTime() - windowStart.getTime()) /\n    3_600_000;\n\n  if (currentPeriod.value === null) {\n    await kv.atomic()\n      .set(currentPeriod.key, new Deno.KvU64(1n), {\n        expireIn: HOUR_IN_MILLISECONDS * 2,\n      })\n      .commit();\n  } else {\n    await kv.atomic()\n      .sum(currentPeriod.key, 1n)\n      .commit();\n  }\n\n  return Math.floor(currentRequests + (previousRequests * previousWeight)) >=\n    MAXIMUM_HOURLY_REQUESTS\n}",
              "syntaxHighlightingTheme": "rose-pine"
            }
          }
        ]
      }
    ]
  },
  "bskyPostRef": {
    "cid": "bafyreiauf4ydtelvuz2zlpud44oj3b4mf7pzq3rqxuoyvl6awp4l2bsdgi",
    "uri": "at://did:plc:57od6g2ic3e3b3kauctjmo3k/app.bsky.feed.post/3lzrjufzwdk2k",
    "commit": {
      "cid": "bafyreidmcfba22h7x5cd6j72t26x7gcwjtzyojb2kl3sijll3iw4isqoyu",
      "rev": "3lzrjug4ckn2q"
    },
    "validationStatus": "valid"
  },
  "description": "If you have a KV cache, why not use it?",
  "publishedAt": "2025-09-26T22:39:13.226Z"
}