Skip to content
113 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails1# Rate limits and log sampling
2
3Internal notes. The public limits are on the docs' rate limits page
4(`apps/docs/src/content/docs/reference/rate-limits.md`); change both, and
5`RATE_LIMITS`, together.
6
7## Rate limits
8
9Every public surface that costs money or sends something is behind a
10[Workers Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/)
11binding (`ratelimits` in its wrangler.jsonc). The table of record is
12`RATE_LIMITS` in `packages/contracts/src/rate-limits.ts`;
13`apps/web/app/lib/front-door-limits.test.ts` fails when a wrangler.jsonc
14and the table disagree, or a namespace id is used twice.
15
16Rules every limit follows:
17
18- **Fails open.** No binding (self-hosted: `deploy/self-host/configs.mjs`
19 does not copy `ratelimits`) or a binding that throws lets the request
20 through, logged as `rate_limit.unavailable` (TS) or
21 `rate limit … could not be asked` (Rust). Helpers: `checkLimit` in
22 packages/contracts, `g1t_kit::limits::check`.
23- **Keys never hold a secret.** Addresses are `ip:<CF-Connecting-IP>`;
24 sessions, tokens, git credentials and email addresses are the first 16 hex
25 digits of their SHA-256.
26- **60-second window.** The binding only counts over 10 or 60 seconds, so
27 "per hour" limits are not possible here; D1 throttles cover those
28 (identity's `throttle.rs`, the waitlist's `rate_limits`, status's resend
29 window).
30- **429 with `Retry-After: 60`.** JSON `{"error": {"code": "rate_limited", …}}`
31 on the API and MCP; plain text for git (git prints it) and pages.
32- Counts are per Cloudflare location and approximate: a limit is a guard
33 against floods, not a quota.
34
35### Namespace ids
36
37Unique per Cloudflare account, in blocks of a hundred per Worker. Take the
38next free id in the Worker's block; a new Worker takes the next block.
39
40| Block | Worker |
41| --- | --- |
42| 41xx | services/packages |
43| 42xx | apps/web |
44| 43xx | services/repos |
45| 44xx | apps/api |
46| 45xx | services/og |
47| 46xx | apps/status |
48
49### Bindings
50
51| Binding | Id | Limit / 60 s | Key | Where enforced |
52| --- | --- | --- | --- | --- |
53| `ANONYMOUS_LIMIT` | 4101 | 300 | address | packages `src/limits.rs`: registry pulls and token requests |
54| `SIGNED_LIMIT` | 4102 | 5,000 | person, workspace or agent | packages, the same |
55| `WEB_ANONYMOUS_LIMIT` | 4201 | 600 | address | web `app/lib/front-door-limits.ts`: signed-out pages and data |
56| `WEB_HEAVY_LIMIT` | 4202 | 30 | address | web: signed-out archives, run pages, logs, artifacts, search |
57| `WEB_SESSION_LIMIT` | 4203 | 1,200 | session cookie hash | web: requests with a session cookie (not checked there) |
58| `WEB_ADDRESS_LIMIT` | 4204 | 3,000 | address | web: every request that reaches the Worker; stops made-up cookies getting round the signed-out limit |
59| `GIT_ANONYMOUS_LIMIT` | 4205 | 120 | address | web: git smart HTTP without `Authorization` (~40 clones) |
60| `GIT_SIGNED_LIMIT` | 4206 | 1,200 | `Authorization` hash | web: git smart HTTP with credentials, sandboxes' included |
A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works.61| `WEB_TOKEN_LIMIT` | 4207 | 1,000 | token hash | web: pages, data requests and socket tickets with `Authorization: Bearer` (a token used on the website, not checked there); the same limit as `API_TOKEN_LIMIT`, counted apart. The live sockets such a page opens carry a ticket instead of the header (`app/lib/socket-ticket.ts`), so their upgrades count as signed out, by address |
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails62| `PACK_FILL_LIMIT` | 4301 | 30 | repository id | repos `src/limits.rs`: packs written to `GIT_PACKS`; past it the pack is streamed, not kept |
63| `ANONYMOUS_FETCH_LIMIT` | 4302 | 120 | repository id | repos: anonymous fetches the git store answers (cache hits never count) |
64| `API_ANONYMOUS_LIMIT` | 4401 | 60 | `rest:`/`mcp:` + address | api `src/limits.rs`: no token, or a wrong one |
65| `API_TOKEN_LIMIT` | 4402 | 1,000 | `rest:`/`mcp:` + token hash | api: with a bearer token |
66| `OG_RENDER_LIMIT` | 4501 | 60 | address | og `src/index.ts`: cache misses only; past it, the brand card (brief cache) |
67| `STATUS_SUBSCRIBE_LIMIT` | 4601 | 3 | address | status `src/index.ts`: `POST /subscribe` |
68| `STATUS_EMAIL_LIMIT` | 4602 | 2 | email hash | status: the same, per address asked for (the D1 resend window also allows one email per 10 minutes) |
69
70What is deliberately not limited:
71
72- Static assets (served by the assets binding before the Worker), avatars,
73 `go get` answers and the docs redirect on the front door.
74- Packages through the front door: the packages service limits itself.
75- On the API: Stripe and connection webhooks, sandbox and runner reports
76 (job, run, check, plan, review, backup and queue tokens; `REPORTS` in
77 `apps/api/src/limits.rs`), the Actions toolkit, OIDC and deployment
78 build reports. Many sandboxes share an egress address.
79- Status has no Turnstile: nothing in g1t uses Turnstile yet. The honeypot,
80 both limits and the resend window are the guard.
81
82Why anonymous git matters: each clone or fetch the store answers is an
83operation billed to the repository's workspace (`services/repos/src/git_ops.rs`)
84and a miss writes a pack to R2 (`pack_cache.rs`). The address limit stops
85one client; `ANONYMOUS_FETCH_LIMIT` bounds many addresses against one
86repository (at most 120 × 60 × 24 ≈ 173k operations a day, about $26 at
87cost), and `PACK_FILL_LIMIT` bounds R2 writes.
88
89## Log sampling
90
91Workers Logs keeps `head_sampling_rate` of invocations (`observability`
92in each wrangler.jsonc). The decision is made when the request starts, so
93an unsampled request's `console.error` and uncaught exception are dropped
94with the rest of its logs. Workers metrics (requests, errors, CPU) in the
95dashboard are not sampled; only the log lines are.
96
97| Rate | Workers | Why |
98| --- | --- | --- |
99| 1 | billing, identity, runner, actions, deployments, status, sudo | Money, sign-in or a run someone will ask about, or too few requests to matter. Every error is kept. |
100| 0.1 | web, api, pages, repos, og, models, search, context, events, work, webhooks, integrations, packages, projects, security, docs | Every page view, git request or event; a tenth is enough to see a pattern. |
101
102The tradeoff: on a 0.1 Worker, a one-off error has a 90% chance of leaving
103no log line. Error counts in the dashboard still show it, and a recurring
104one shows up within a few occurrences. To chase a rare error on one of
105those Workers, raise its rate to 1 for the investigation and lower it after.
A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works.106
107A sampled invocation's log holds its full URL. The only credential g1t
108ever puts in a URL is a socket ticket (`?ticket=` on `/-/live`,
109`/<workspace>/-/chat/live` and `/<workspace>/-/artifacts/live`, for pages
110opened with an access token; `apps/web/app/lib/socket-ticket.ts`). The site
111never logs it or passes it on, and one found in Workers Logs is useless:
112it lasts 60 seconds, opens only that socket, and the token inside it is
113sealed and checked again when the socket opens.

This file's history is long; its oldest lines are credited to the oldest commit read.