Skip to content
104 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 |
61| `PACK_FILL_LIMIT` | 4301 | 30 | repository id | repos `src/limits.rs`: packs written to `GIT_PACKS`; past it the pack is streamed, not kept |
62| `ANONYMOUS_FETCH_LIMIT` | 4302 | 120 | repository id | repos: anonymous fetches the git store answers (cache hits never count) |
63| `API_ANONYMOUS_LIMIT` | 4401 | 60 | `rest:`/`mcp:` + address | api `src/limits.rs`: no token, or a wrong one |
64| `API_TOKEN_LIMIT` | 4402 | 1,000 | `rest:`/`mcp:` + token hash | api: with a bearer token |
65| `OG_RENDER_LIMIT` | 4501 | 60 | address | og `src/index.ts`: cache misses only; past it, the brand card (brief cache) |
66| `STATUS_SUBSCRIBE_LIMIT` | 4601 | 3 | address | status `src/index.ts`: `POST /subscribe` |
67| `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) |
68
69What is deliberately not limited:
70
71- Static assets (served by the assets binding before the Worker), avatars,
72 `go get` answers and the docs redirect on the front door.
73- Packages through the front door: the packages service limits itself.
74- On the API: Stripe and connection webhooks, sandbox and runner reports
75 (job, run, check, plan, review, backup and queue tokens; `REPORTS` in
76 `apps/api/src/limits.rs`), the Actions toolkit, OIDC and deployment
77 build reports. Many sandboxes share an egress address.
78- Status has no Turnstile: nothing in g1t uses Turnstile yet. The honeypot,
79 both limits and the resend window are the guard.
80
81Why anonymous git matters: each clone or fetch the store answers is an
82operation billed to the repository's workspace (`services/repos/src/git_ops.rs`)
83and a miss writes a pack to R2 (`pack_cache.rs`). The address limit stops
84one client; `ANONYMOUS_FETCH_LIMIT` bounds many addresses against one
85repository (at most 120 × 60 × 24 ≈ 173k operations a day, about $26 at
86cost), and `PACK_FILL_LIMIT` bounds R2 writes.
87
88## Log sampling
89
90Workers Logs keeps `head_sampling_rate` of invocations (`observability`
91in each wrangler.jsonc). The decision is made when the request starts, so
92an unsampled request's `console.error` and uncaught exception are dropped
93with the rest of its logs. Workers metrics (requests, errors, CPU) in the
94dashboard are not sampled; only the log lines are.
95
96| Rate | Workers | Why |
97| --- | --- | --- |
98| 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. |
99| 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. |
100
101The tradeoff: on a 0.1 Worker, a one-off error has a 90% chance of leaving
102no log line. Error counts in the dashboard still show it, and a recurring
103one shows up within a few occurrences. To chase a rare error on one of
104those Workers, raise its rate to 1 for the investigation and lower it after.

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