> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Turbo Remote Cache

> Self-hosted Turborepo remote cache on Cloudflare R2: trust model, configuration, cost bounds, cutover, and rollback.

`apps/turbo-cache` is a Cloudflare Worker that implements the Turborepo remote
cache API (`/v8/artifacts`) on an R2 bucket. It replaces the Vercel-hosted
remote cache, which stays the fallback until cutover. It ships **disabled**:
`TURBO_CACHE_MODE` defaults to `off`, which serves nothing and performs no
billable storage operations. For cache layers and miss debugging, see
the Caching page (`apps/docs/build/devops/caching.mdx`).

## Design

* **Keys:** artifacts are stored at `v1/<scope>/<hash>`. The scope comes from
  the server-side token registry, never from the client's `teamId` or `slug`,
  so a client cannot choose where its artifacts land.
* **Tokens:** `TURBO_CACHE_TOKENS` is a JSON list of
  `{ "id", "sha256", "read": [scopes], "write"?: scope }`. The registry stores
  only SHA-256 digests of tokens and compares them in constant time. A writer
  may write only the first scope it reads. One malformed entry disables the
  whole registry (fail closed).
* **Signatures:** with `TURBO_CACHE_REQUIRE_SIGNATURE=true` (the default),
  uploads without an `x-artifact-tag` are rejected. Clients sign and verify
  with `TURBO_REMOTE_CACHE_SIGNATURE_KEY` when `turbo.json` sets
  `remoteCache.signature: true`, so a stored artifact that was not signed with
  the shared key is rejected by every reader.
* **Bounds:** artifacts are capped at 100 MiB (ceiling 500 MiB). Artifacts
  older than the TTL (default 7 days, ceiling 30) are served as misses even if
  the R2 lifecycle rule lags. Batch queries accept at most 50 hashes. A rate
  limiter allows 600 requests per token per 10 seconds.
* **Durable stop fence:** `budget-v2` in the global `TurboCacheBudget`
  Durable Object is the authoritative budget and fence. Every reservation and
  admin mutation reads and writes it in one storage transaction. DO output
  gates keep responses behind durable commits. Missing, malformed or unreadable
  state fails closed; no positive allowance is cached in memory.
  * Daily class A, class B and write-byte caps cover artifact R2 operations
    and deny reservations until the next UTC day. Exhausting daily class A
    or write bytes blocks uploads while reads remain available within class B;
    status stays `enabled` while reads can proceed. The monthly fence stops both.
  * Exhausting the monthly class A allowance persists a fence that survives
    month rollover and DO/Worker restarts until an authenticated operator reset.
  * Charges commit before artifact access. There are no application admission
    retries, refunds, alarms or checkpoint jobs. An ambiguous response denies
    access and retains any committed charge.
  * Denials and unchanged admin actions do not write. A 60-second negative
    memo saves repeated DO calls within an isolate; fresh isolates consult the
    same transactional authority. Denied reads return a miss; denied writes
    return `503`.
  * R2 `_control/budget-v1` is a read-only migration source, never a live
    checkpoint or admission source. No control-key PUTs occur, so Turbo bursts
    cannot hit R2's same-key write limit. Delayed legacy PUTs cannot overwrite
    the DO fence or counters after migration.
* **Admin:** `GET /admin/budget` and `POST /admin/fence`
  (`{"action":"trip"}` or `{"action":"reset"}`) require the admin token, whose
  SHA-256 is `TURBO_CACHE_ADMIN_TOKEN_SHA256`. The first fence action imports
  legacy R2 counters if present; a failed/invalid import fails closed. Status
  reads DO state without reserving; events are discarded without storage work.

The authority boundary follows Cloudflare's
[storage transaction guarantees](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#transaction).
R2's [last-completing-writer semantics](https://developers.cloudflare.com/r2/reference/consistency/)
and [same-key write limit](https://developers.cloudflare.com/r2/platform/limits/)
are why live control writes are removed.

## Trust model

| Client | Token | Reads | Writes |
| - | - | - | - |
| Trusted `main` and `production` CI jobs | `gha-main` | `main` | `main` |
| Same-repository branch CI (optional, needs CI security owner sign-off) | `gha-branches` | `branches`, `main` | `branches` |
| Fork pull requests and Dependabot | none (GitHub withholds secrets) | — | — |
| Trusted local machines and devboxes | one token per machine | `local-<id>`, `main` | `local-<id>` |
| Untrusted agent sandboxes | read-only token | `main` | — |

`main` artifacts are written only by trusted `main` CI, so branch, machine and
sandbox clients can reuse them but never poison them. Clients can additionally
enforce read-only at the client with `TURBO_REMOTE_CACHE_READ_ONLY=1` (or
`TURBO_CACHE=local:rw,remote:r`).

## Configuration names

Set these values by name only. Never commit them.

Worker secrets (owner creates them with the cf CLI or the dashboard):

* `TURBO_CACHE_TOKENS`: JSON token registry, digests only.
* `TURBO_CACHE_ADMIN_TOKEN_SHA256`: digest of the admin token.

Worker variables (in `apps/turbo-cache/cloudflare.config.ts`):

* `TURBO_CACHE_MODE`: `off` (default), `read-only`, or `read-write`.
* `TURBO_CACHE_REQUIRE_SIGNATURE`, `TURBO_CACHE_TTL_DAYS`,
  `TURBO_CACHE_MAX_ARTIFACT_BYTES`, `TURBO_CACHE_DAILY_CLASS_A`,
  `TURBO_CACHE_DAILY_CLASS_B`, `TURBO_CACHE_DAILY_WRITE_BYTES`,
  `TURBO_CACHE_MONTHLY_CLASS_A_FENCE`. Missing or malformed values fall back
  to defaults, and overrides are clamped to hard ceilings.

Cloudflare resources (owner creates):

* R2 bucket `tuturuuu-turbo-cache` with a lifecycle rule that deletes objects
  under `v1/` after 7 days. Exclude `_control/` from expiry and cleanup. Never
  delete retained migration evidence or DO budget storage to clear a fence or
  recover an outage.
* Custom domain `turbo-cache.tuturuuu.com` on the Worker.

GitHub repository settings (owner sets at cutover):

* Variable `TURBO_API`: `https://turbo-cache.tuturuuu.com`.
* Variable `TURBO_TEAM`: any stable slug. The Worker ignores it for scoping.
* Secret `TURBO_TOKEN`: the `gha-main` token, replacing the Vercel token.
* Secret `TURBO_REMOTE_CACHE_SIGNATURE_KEY`: at least 32 random bytes, shared
  by every trusted client.

Local trusted machines (shell profile or secret store, never `.env`):
`TURBO_API`, `TURBO_TEAM`, `TURBO_TOKEN`, and
`TURBO_REMOTE_CACHE_SIGNATURE_KEY`.

## Cost estimate

From the 2026-10-16 to 2026-10-23 CI inventory:

* Test, Type Check, Coverage, Mail Calendar Tasks, Meet and Parley run about
  303,000 Turbo task lookups per week. Vercel and release builds add about
  20,000.
* Runs on `main` produce about 40,000 of those lookups. Those are the only
  writes under the default trust model.

A successful lookup costs one Worker request, one Durable Object request,
one transactional budget update and up to one R2 artifact GET/HEAD per readable
scope. An upload reserves one artifact PUT and its declared bytes. A failed
artifact write retains its reservation. No request writes an R2 control object.

A batch reserves `hashes × readable scopes` HEADs. The largest accepted batch
is 50 hashes across four scopes: 200 artifact HEADs, one DO call and one
rate-limit call, a conservative 202-operation envelope under the configured
208-subrequest ceiling. There is no retry fan-out.

The default monthly 2M class A allowance covers artifact PUTs. The daily
100,000 class A, 1M class B and 20 GiB write-byte allowances cover artifact
operations/bytes. DO requests, duration and transactional storage have separate
pricing and are outside these R2 counters. Validate current account pricing
and measured traffic before enablement; the historical lookup inventory above
is an input to that review, not a current bill prediction.

These are bounds on successful artifact reservations, not a total bill cap.
Denied fresh isolates and status requests still cost Worker/DO requests and
storage reads. Denials and unchanged admin actions do not write. Failed attempts
can consume resources; each request attempts admission once, while ingress is
unbounded. Use `TURBO_CACHE_MODE=off` to stop artifact-path storage/admission
activity and disable the Worker route to stop invocations. Rate limits, CPU
caps and alerts do not cap total spend.

`src/stop-regression.test.ts` and `src/budget-integrity.test.ts` cover concurrent
reservations, zero denial writes, failed/unknown transactions, stop/restart,
legacy PUT completion after a successful stop, a 600-request burst with simulated
R2 control-write 429s, migration and the maximum batch cost. These use in-memory
R2/DO fixtures. The real workerd check below covers native transactional
initialization, fence writes, duplicate actions and persistence across restart.

## CI qualification

`.github/workflows/turbo-cache-cloudflare.yaml` runs automatically for cache,
lockfile, toolchain and runtime-harness changes. Its `build-acceptance` job
checks out the immutable PR head, runs types, lint, format and unit checks,
then builds with `bun run build` without restoring compiled artifacts. It runs
`node --test apps/turbo-cache/scripts/verify-worker-runtime.mjs` against both
the source Worker and the emitted cf Build Output (the latter with
`TURBO_CACHE_TEST_BUILD_OUTPUT=1`), retaining the accepted output under the head
SHA. This workflow qualifies artifacts only and has no publication job.

The real local workerd checks exercise the default-disabled API, unauthorized
admin rejection, missing-state refusal, authenticated transactional initialization through
the Durable Object, repeated trip, fence persistence across workerd restart and
operator reset. They use random disposable credentials, loopback HTTP and an
isolated temporary persistence directory cleaned on close or failure. Run the
source check under resource admission; builds and emitted-output checks run in
exact-commit CI. Enabled artifact traffic, denial operation counts and maximum
batch bounds have fixture coverage above. Hosted routing, rate-limit behavior,
account pricing and live storage/stop acceptance remain release-owner gates.

## Cutover checklist

1. Owner creates the R2 bucket, its 7-day `v1/` lifecycle rule, the custom
   domain, and the two Worker secrets. Generate tokens locally, store only
   their SHA-256 digests in `TURBO_CACHE_TOKENS`, and hand the raw tokens to
   their holders through the secret store.

2. Deploy with `TURBO_CACHE_MODE=off`. For an existing service, disable and
   drain the previous deployment first. Reconcile every unknown legacy R2 PUT
   before importing usage; a missing/readable old snapshot does not prove an
   in-flight operation failed. Preserve the previous counters and fence as
   migration evidence. First initialization uses authenticated
   `POST /admin/fence` with `{"action":"trip"}` to import the legacy snapshot
   into DO storage while remaining fenced. If there is no prior usage, it seeds
   empty counters and trips. Verify `GET /admin/budget` against the reviewed
   usage, then explicitly reset the fence when enabling is authorized. An older
   DO-local budget needs reviewed migration into this state; do not erase it.
   Never initialize empty counters as outage recovery. Confirm status is
   `disabled` and disabled artifact requests add no R2/DO operations. The first
   admin initialization reads legacy R2 once and commits one DO transaction;
   subsequent actions do no R2 control work. Keep the same DO identity/binding
   and `budget-v2` storage across upgrades and rollback. Rolling back to an
   R2-authoritative version requires a separate reviewed counter migration,
   with service disabled and requests drained.

3. Set `TURBO_CACHE_MODE=read-write`. From one trusted machine, run a small
   task twice with the new `TURBO_API` and token, then confirm a `remote` hit
   and the budget counters (`GET /admin/budget`).

4. Pass `api: ${{ vars.TURBO_API }}` and
   `signature-key: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}` to
   `.github/actions/run-with-turbo-remote-cache` under the existing trusted
   conditions. Set the repository variable and secrets. Replace
   `TURBO_TOKEN` with the `gha-main` token.

5. Enable `remoteCache.signature: true` and
   `futureFlags.longerSignatureKey: true` in `turbo.json`. Existing unsigned
   artifacts then miss once and are rewritten signed.

6. Watch one week of budget counters against this estimate before adding
   branch or machine writers.

## Rollback

* **Durable pause:** `POST /admin/fence` with `{"action":"trip"}`. New
  artifact requests consult the DO and skip artifact I/O; Worker/DO reads
  continue. Already reserved work may
  finish.
* **Independent storage/admission stop:** set `TURBO_CACHE_MODE=off`, including
  when the DO is unavailable. Drain the prior revision. Ordinary disabled
  artifact requests perform no R2 or DO calls. Disable the Worker route as
  well when ingress itself must stop. Clients fall back to running tasks.
* **Return to Vercel:** delete the `TURBO_API` variable, which makes the
  wrapper unset `TURBO_API`, and restore the Vercel token to `TURBO_TOKEN`.
  Keep the Vercel token in the owner's secret store until the self-hosted
  cache has run a full week within budget.
* Signed artifacts are not required for the Vercel fallback. Revert step 5 if
  Vercel clients lack the signature key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.