Rate limiting
Limit how often one reader can call the assistant, the API playground proxy, and server-side search, on by default, with the count kept in memory, Upstash, Cloudflare, or Unkey.
The server routes a reader can call cost you something each time: the assistant spends model tokens, the API playground proxy relays requests to your API, and Mixedbread search runs a paid query. Blume limits how often one reader, identified by IP address, can call each of them. A reader over the limit gets 429 Too Many Requests with a Retry-After header, and the assistant tells them to try again in a few minutes.
Rate limiting is on by default, at 30 requests per reader per route every 10 minutes: well above what a person sends to the assistant or the playground, and low enough to stop a script. Search counts differently. The search dialog sends a query each time the reader pauses while typing, so one search can take several requests, and a reader who searches steadily for a few minutes can reach the limit. If your site uses Mixedbread search, raise requests, which applies to every route. Rate limiting only matters for a server build; a static site has no server routes to limit.
import { defineConfig } from "blume";
import { upstash } from "blume/ratelimit";
export default defineConfig({
rateLimit: upstash({ requests: 30, window: 600 }), // window in seconds
});
Each route keeps its own count, so a busy playground session doesn’t use up the reader’s assistant questions.
Where the count lives
| Adapter | Count kept in | Limit holds |
|---|---|---|
memory() |
The server’s memory (the default) | Exactly on one server; per instance on serverless hosts |
upstash() |
Upstash Redis | Exactly, on every host |
cloudflare() |
Cloudflare’s Workers rate limiting | Per Cloudflare location |
unkey() |
Unkey | On every host; regions catch up within moments |
Memory
memory() needs nothing. On a server that runs as one process, like deployment: node(), the count is exact. Vercel, Netlify, and Cloudflare run many short-lived instances, and each keeps its own count, so there it stops a burst from one reader rather than enforcing the limit exactly. For an exact limit on those hosts, use one of the shared stores below.
import { memory } from "blume/ratelimit";
rateLimit: memory({ requests: 60, window: 600 }),
Upstash
upstash() keeps the count in Upstash Redis, which every instance shares. Create a database, then set its REST endpoint and token as UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. Blume talks to Upstash’s REST API, so there’s nothing to install. Until both are set, the routes count in memory and blume build warns.
import { upstash } from "blume/ratelimit";
rateLimit: upstash(),
A database added from Vercel’s Marketplace sets its endpoint and token as KV_REST_API_URL and KV_REST_API_TOKEN instead, with any prefix you chose when you connected it. Name the variables with urlEnv and tokenEnv:
rateLimit: upstash({ urlEnv: "KV_REST_API_URL", tokenEnv: "KV_REST_API_TOKEN" }),
Cloudflare
cloudflare() counts with Workers rate limiting, for a site deployed with deployment: cloudflare(). Blume declares the binding in the Worker’s config when it builds, so there’s nothing to set up. Cloudflare counts per location and takes a window of 10 or 60 seconds, so its defaults are 10 requests per 60 seconds.
import { cloudflare } from "blume/deploy";
import { cloudflare as cloudflareRateLimit } from "blume/ratelimit";
export default defineConfig({
deployment: cloudflare(),
rateLimit: cloudflareRateLimit({ requests: 10, window: 60 }),
});
The binding’s namespace comes from the Worker’s name, so two sites on one account don’t share a count. Set namespaceId to choose it yourself.
Unkey
unkey() keeps the count in Unkey’s rate limiting, which every instance shares. Create a root key with the ratelimit.*.limit permission, plus ratelimit.*.create_namespace so Unkey can create the namespace on the first request, then set it as UNKEY_ROOT_KEY. Blume talks to Unkey’s REST API, so there’s nothing to install. Until the key is set, the routes count in memory and blume build warns.
import { unkey } from "blume/ratelimit";
rateLimit: unkey(),
The namespace is docs unless you name another with namespace. Name the variable that holds the key with rootKeyEnv:
rateLimit: unkey({ namespace: "acme-docs", rootKeyEnv: "DOCS_UNKEY_ROOT_KEY" }),
Unkey counts in each region and shares the counts between regions within moments, so a short burst split across regions can get a few more requests through than the limit. Every count fails with a key Unkey refuses, like one missing those permissions, and a request a shared store fails to count is let through, so if the server logs Rate limiting failed, check the key.
Behind a reverse proxy
Vercel, Netlify, and Cloudflare tell Blume each reader’s address. A node() server behind a reverse proxy, like nginx or Caddy, sees every request come from the proxy, so every reader shares one count and a busy site hits the limit for everyone. List the host the proxy answers for in allowedDomains:
import { node } from "blume/deploy";
deployment: node({
site: "https://docs.example.com",
allowedDomains: [{ hostname: "docs.example.com" }],
}),
This is Astro’s security.allowedDomains: for a request whose Host or X-Forwarded-Host header matches an entry, Astro takes the reader’s address from the first entry of X-Forwarded-For. Have the proxy pass the site’s host through and replace any X-Forwarded-For the reader sent with the address it sees, like nginx’s proxy_set_header X-Forwarded-For $remote_addr;. A proxy that appends to the header instead lets a reader pick their own address and get a fresh count with every request. Leave allowedDomains unset when readers reach the server directly.
Turning it off
Set rateLimit: false to let every request through, when your host’s firewall already limits these routes, say. A request whose address the host can’t tell is always let through, and so is one a shared store fails to count: the failure is logged, and readers never get locked out by a store outage.
For the assistant, pair rate limiting with a bot check to stop scripts that spread their requests across many addresses.