DocupDocup
Esc

Rate limiting

How Docup counts requests in Redis, which limits exist, and how each one behaves when Redis is down.

AI work costs money and Ask AI is open to the world, so every expensive path is metered. The rate limiting implementation is small and efficient.

Fixed windows#

A limit is { key, max, seconds }. countRequest(...limits) increments one counter per limit in a single Redis pipeline round trip:

Text
limit:<key>:<floor(now / seconds)>

Each counter is set to expire after seconds. Because the window number is part of the key, windows are fixed: a day is the UTC day, and an hour starts on the hour. The function returns how many requests are left in each limit; below zero means over.

Fixed windows are cheaper than sliding ones and good enough here. A burst can straddle midnight and use two days' allowance in a short time, which is acceptable for these limits.

Redis connection and retries#

In production, Docup connects to Upstash Redis over HTTP using the environment variables UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. In local development, it uses a REST proxy.

Every call is its own HTTP request, with one client serving the whole Cloudflare Worker. When a call fails, the client retries the request up to two times, 100 ms apart. This is much faster than the default five retries with growing backoff, ensuring that a struggling Redis will not keep a Worker request waiting long.

Two policies#

Helper When Redis fails Used for
withinLimits and withinDailyUpdates Logs and allows the action Signed-in actions (lookups, runs, retries, publishes), Better Auth's own limits, and AI updates after a push
countRequest (called directly) Throws, and the route returns a 503 Service Unavailable response Ask AI, which anyone can call

A known account or a background repository update can do little harm in a few minutes, so these actions favor availability. Ask AI cannot run without Redis, because nothing else would keep its cost in check.

The limits#

Limit Key Max Window
Repository lookups github:<org> 60 1 hour
Docs runs docs-run:<org> 10 1 day
Page retries page-retry:<org> 50 1 day
Publishes publish:<org> 50 1 day
AI updates after a push limit:docs-update:<org>:<day> 15 1 day
Ask AI burst ask-burst:<visitor> 20 1 minute
Ask AI per visitor and site ask:<project>:<visitor> 15 1 day
Ask AI per site ask-site:<project> 500 1 day
Ask AI everywhere ask-all 5,000 1 day
Better Auth auth:<key> Per rule Per rule

The update counter is incremented from a Trigger.dev task, which uses withinDailyUpdates to write the same key format directly. The task counts an update only when it has AI work to do.

Identifying visitors#

Cloudflare sets cf-connecting-ip, and a client cannot change it. It is missing only in local development, where local is used.

One person usually owns a whole IPv6 /64, so visitorOf expands the address and keeps its first four groups as a:b:c:d::/64. IPv4 addresses are used as they are.

Webhook de-duplication#

The same Redis client stores github-delivery:<id> for 24 hours with SET NX, so a webhook delivery GitHub sends twice is processed once. See GitHub webhook.

Documentation written with Docup