{"protocol":"clank-doc/1","frameworkVersion":"0.9.4","slug":"usage-and-limits","title":"Usage accounting and traffic limits","description":"Clank records a small, durable monthly usage ledger at managed ingress. The ledger gives workspace members and automation one stable view of consumed request capacity, known transfer, rejected requests, and the limits currently enforced. It","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/usage-and-limits","source":"docs/usage-and-limits.md","headings":["Inspect usage","API contract","What a request means","What known transfer means","Completeness and upgrades","Retention, deletion, and privacy","Configure defaults","Deployment boundary","Billing boundary"],"tableOfContents":[{"id":"inspect-usage","title":"Inspect usage","level":2},{"id":"api-contract","title":"API contract","level":2},{"id":"what-a-request-means","title":"What a request means","level":2},{"id":"what-known-transfer-means","title":"What known transfer means","level":2},{"id":"completeness-and-upgrades","title":"Completeness and upgrades","level":2},{"id":"retention-deletion-and-privacy","title":"Retention, deletion, and privacy","level":2},{"id":"configure-defaults","title":"Configure defaults","level":2},{"id":"deployment-boundary","title":"Deployment boundary","level":2},{"id":"billing-boundary","title":"Billing boundary","level":2}],"markdown":"# Usage accounting and traffic limits\n\nClank records a small, durable monthly usage ledger at managed ingress. The ledger gives workspace\nmembers and automation one stable view of consumed request capacity, known transfer, rejected\nrequests, and the limits currently enforced. It is deliberately transparent: the API describes\nthe measurement boundary and never turns incomplete network information into an estimated charge.\n\nUsage controls are part of the existing control-plane process and SQLite catalog. They do not\nrequire a billing service, Redis, telemetry vendor, or additional Railway resource.\n\n## Inspect usage\n\nOpen **Usage** in the control-plane workspace navigation. Choose a workspace and UTC month to see:\n\n- admitted requests and the monthly request limit;\n- known transfer and the monthly known-transfer limit;\n- requests rejected by a traffic limit;\n- the per-project requests-per-minute ceiling;\n- a production/preview project breakdown, including bounded history for deleted projects; and\n- current projects, previews, members, domains, releases, and retained release storage.\n\nThe route is refresh-safe and server renders the correct authenticated Usage shell at `/usage`.\nIts project table becomes labelled cards on narrow screens.\n\nThe same contract is available from the CLI:\n\n```sh\nclank usage\nclank usage --month=2026-07\nclank usage --org=<workspace-id>\nclank usage --org=<workspace-id> --month=2026-07 --json\n```\n\nInside a linked project, `clank usage` resolves the project's workspace. Outside a linked\ndirectory, it selects the account's only accessible workspace or asks for `--org` when the choice\nis ambiguous. `--json` emits the server's unmodified `clank-usage/1` document.\n\n## API contract\n\nAn authenticated account or account-wide token can request:\n\n```http\nGET /api/usage?organizationId=<workspace-id>&month=2026-07\n```\n\n`month` is optional and defaults to the current UTC month. It must use `YYYY-MM` and remain within\nthe configured retention window. The caller must still belong to the workspace. A token scoped to\nanother workspace is denied, and project-scoped tokens cannot read workspace-wide usage.\n\nThe response shape is:\n\n```json\n{\n  \"ok\": true,\n  \"protocol\": \"clank-usage/1\",\n  \"workspace\": {\n    \"id\": \"organization_...\",\n    \"name\": \"Personal\",\n    \"slug\": \"personal\"\n  },\n  \"period\": {\n    \"key\": \"2026-07\",\n    \"startedAt\": 1782864000000,\n    \"endsAt\": 1785542400000,\n    \"current\": true,\n    \"closed\": false,\n    \"complete\": true,\n    \"trackingStartedAt\": 1782864000000,\n    \"timezone\": \"UTC\"\n  },\n  \"usage\": {\n    \"requests\": 1200,\n    \"requestBytes\": 31000,\n    \"responseBytes\": 2000000,\n    \"knownTransferBytes\": 2031000,\n    \"rejectedRequests\": 3\n  },\n  \"limits\": {\n    \"requests\": 5000000,\n    \"knownTransferBytes\": 107374182400,\n    \"requestsPerMinutePerProject\": 3000\n  },\n  \"remaining\": {\n    \"requests\": 4998800,\n    \"knownTransferBytes\": 107372151400\n  },\n  \"resources\": {\n    \"asOf\": 1785000000000,\n    \"projects\": 2,\n    \"previews\": 1,\n    \"members\": 1,\n    \"domains\": 1,\n    \"releases\": 4,\n    \"releaseStorageBytes\": 8000000\n  },\n  \"projects\": [],\n  \"retentionMonths\": 24,\n  \"metering\": {\n    \"requestBoundary\": \"managed_ingress_admission\",\n    \"transferBoundary\": \"request_body_and_declared_response_content_length\",\n    \"streamedResponseBytesKnown\": false,\n    \"pricingIncluded\": false\n  }\n}\n```\n\nTimestamps are Unix milliseconds. `resources` is a current inventory taken at `asOf`; it is not a\nhistorical reconstruction for the selected month.\n\n## What a request means\n\nA request enters the monthly and minute ledgers only after:\n\n1. exact-host routing selects an active project;\n2. the managed-ingress request body passes the configured size and UTF-8-independent byte reader;\n3. the admission policy resolves the project's current workspace limits; and\n4. one SQLite `BEGIN IMMEDIATE` transaction verifies and increments both applicable counters.\n\nThat transaction serializes simultaneous admissions across control-plane connections sharing the\nsame SQLite catalog. At an exact boundary, only the permitted number of requests is admitted.\n\nThe monthly limit is shared by production projects and previews in one workspace. The minute limit\nis separate for each project and aligned to a UTC wall-clock minute. A denied request does not\nincrement admitted requests or request bytes, but does increment `rejectedRequests`.\n\nDenials use HTTP `429`, a bounded `Retry-After`, and one stable code:\n\n| Code | Boundary |\n| --- | --- |\n| `PROJECT_RATE_LIMIT_REACHED` | Project request ceiling for the current UTC minute |\n| `WORKSPACE_REQUEST_LIMIT_REACHED` | Workspace admitted-request ceiling for the current UTC month |\n| `WORKSPACE_TRANSFER_LIMIT_REACHED` | Workspace known-transfer ceiling for the current UTC month |\n\nIf admission storage or policy validation fails, ingress fails closed with HTTP `503`,\n`ADMISSION_UNAVAILABLE`, and a short retry hint. Private storage errors are not returned.\n\n## What known transfer means\n\nKnown transfer is:\n\n```text\nadmitted request-body bytes\n+ Content-Length declared by admitted responses that can carry a body\n```\n\n`HEAD`, `204`, and `304` responses contribute zero response bytes even if a representation-length\nheader is present. Clank does not buffer application responses merely to count them. A streamed,\ncompressed-on-the-fly, or otherwise undeclared response therefore contributes zero known response\nbytes.\n\nThe transfer check can include the complete request body before upstream work begins. Response\nlength becomes known only after upstream response headers arrive, so one or more already-admitted\nconcurrent responses can move the ledger past the monthly limit. Later requests are blocked.\nOperators that need a hard network-cost ceiling must additionally bound response sizes and\nconcurrency at the application or edge.\n\nRequests rejected before tenant admission—unknown hosts, inactive routes, oversized bodies, and\nmalformed proxy input—remain in edge/host diagnostics rather than a tenant ledger. Keep public\ningress behind WAF, DDoS, connection, and body-rate protection; monthly application quotas are not\na substitute for those controls.\n\n## Completeness and upgrades\n\nOn the first upgrade that creates the usage ledger, Clank seeds the current month from retained\nminute metrics and records `trackingStartedAt`. Metrics do not distinguish historical admission\nrejections, and metrics older than their own retention may already be gone. For that reason:\n\n- `period.complete` is `false` when tracking began after the requested month started;\n- the console and CLI display a partial-history warning; and\n- callers must not invent missing values.\n\nThe first full UTC month after tracking begins is complete. The current month can be complete even\nthough it is not closed; `current`, `closed`, and `complete` are separate fields.\n\n## Retention, deletion, and privacy\n\nMonthly rows are retained for the current month plus the configured number of previous calendar\nmonths. Pruning runs at control-plane startup and opportunistically during ingress, so reducing\nretention takes effect without requiring a new deployment.\n\nPermanent project deletion removes application data, detailed ingress metrics, releases, secrets,\nlogs, domains, and tokens through the existing deletion lifecycle. Its bounded monthly usage rows\nremain until usage retention expires so workspace totals stay auditable. Those rows contain only:\n\n- workspace and project IDs;\n- the last project name, slug, and production/preview kind;\n- UTC month;\n- admitted request, request-byte, declared-response-byte, and rejection totals; and\n- the last ledger-update timestamp.\n\nThe ledger never stores URL paths, hostnames, query strings, headers, cookies, authorization\nvalues, IP addresses, user agents, request/response bodies, user email addresses, or application\nrecord identities. Removing a member immediately removes their API access without rewriting\nworkspace totals.\n\n## Configure defaults\n\nSelf-hosted defaults are:\n\n| Environment variable | Default | Valid range |\n| --- | ---: | ---: |\n| `CLANK_MAX_REQUESTS_PER_MONTH_PER_ORGANIZATION` | `5000000` | 1 through JavaScript's safe integer maximum |\n| `CLANK_MAX_TRANSFER_BYTES_PER_MONTH_PER_ORGANIZATION` | `107374182400` | 1 byte through JavaScript's safe integer maximum |\n| `CLANK_MAX_REQUESTS_PER_MINUTE_PER_PROJECT` | `3000` | 1–1,000,000 |\n| `CLANK_USAGE_RETENTION_MONTHS` | `24` | 1–120 |\n\nThe first three values participate in the existing quota hierarchy:\n\n1. an explicit workspace override;\n2. otherwise the workspace owner's account override; and\n3. otherwise the installation environment default.\n\nPlatform administrators edit those overrides through **Control plane → Accounts → Limits…**.\nChanges apply on the next admission. Lowering a limit never deletes an application resource.\nRetention is installation-wide rather than tenant-adjustable.\n\nProgrammatic installations pass the equivalent `PlatformLimits` fields to `openPlatform()`:\n\n```ts\nconst platform = await openPlatform({\n  // ...\n  limits: {\n    requestsPerMonthPerOrganization: 5_000_000,\n    transferBytesPerMonthPerOrganization: 100 * 1024 * 1024 * 1024,\n    requestsPerMinutePerProject: 3_000,\n    usageRetentionMonths: 24,\n  },\n});\n```\n\n## Deployment boundary\n\nOnly traffic passing through Clank managed ingress is metered and limited. Do not expose supervised\napplication ports publicly or route around the control plane. The supported production topology\nbinds application ports to loopback/private networking and exposes only the TLS edge in front of\nClank.\n\nIf managed ingress is disabled, applications continue to run but the monthly usage ledger has no\ntraffic to observe. `resources` still reports current control-plane inventory.\n\n## Billing boundary\n\nClank core does not contain prices, currencies, subscriptions, invoices, taxes, payment-provider\nidentifiers, or automatic suspension for unpaid accounts. The ledger is stable input for a future\nexternal entitlement/billing adapter, not a monetary record. Such an adapter must version its own\npricing, preserve raw byte/request units, handle late or incomplete periods explicitly, and never\nreinterpret `knownTransferBytes` as total network egress.\n"}