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’steamIdorslug, so a client cannot choose where its artifacts land. - Tokens:
TURBO_CACHE_TOKENSis 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 anx-artifact-tagare rejected. Clients sign and verify withTURBO_REMOTE_CACHE_SIGNATURE_KEYwhenturbo.jsonsetsremoteCache.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-v2in the globalTurboCacheBudgetDurable 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
enabledwhile 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-v1is 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.
- 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
- Admin:
GET /admin/budgetandPOST /admin/fence({"action":"trip"}or{"action":"reset"}) require the admin token, whose SHA-256 isTURBO_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.
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.
apps/turbo-cache/cloudflare.config.ts):
TURBO_CACHE_MODE:off(default),read-only, orread-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.
- R2 bucket
tuturuuu-turbo-cachewith a lifecycle rule that deletes objects underv1/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.comon the Worker.
- Variable
TURBO_API:https://turbo-cache.tuturuuu.com. - Variable
TURBO_TEAM: any stable slug. The Worker ignores it for scoping. - Secret
TURBO_TOKEN: thegha-maintoken, replacing the Vercel token. - Secret
TURBO_REMOTE_CACHE_SIGNATURE_KEY: at least 32 random bytes, shared by every trusted client.
.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
mainproduce about 40,000 of those lookups. Those are the only writes under the default trust model.
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
-
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 inTURBO_CACHE_TOKENS, and hand the raw tokens to their holders through the secret store. -
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 authenticatedPOST /admin/fencewith{"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. VerifyGET /admin/budgetagainst 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 isdisabledand 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 andbudget-v2storage across upgrades and rollback. Rolling back to an R2-authoritative version requires a separate reviewed counter migration, with service disabled and requests drained. -
Set
TURBO_CACHE_MODE=read-write. From one trusted machine, run a small task twice with the newTURBO_APIand token, then confirm aremotehit and the budget counters (GET /admin/budget). -
Pass
api: ${{ vars.TURBO_API }}andsignature-key: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}to.github/actions/run-with-turbo-remote-cacheunder the existing trusted conditions. Set the repository variable and secrets. ReplaceTURBO_TOKENwith thegha-maintoken. -
Enable
remoteCache.signature: trueandfutureFlags.longerSignatureKey: trueinturbo.json. Existing unsigned artifacts then miss once and are rewritten signed. - Watch one week of budget counters against this estimate before adding branch or machine writers.
Rollback
- Durable pause:
POST /admin/fencewith{"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_APIvariable, which makes the wrapper unsetTURBO_API, and restore the Vercel token toTURBO_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.