Skip to main content
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. R2’s last-completing-writer semantics and same-key write limit are why live control writes are removed.

Trust model

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.