Dogfooding at scale: migrating cdnjs to Cloudflare's Developer Platform¶
Summary¶
Cloudflare migrated cdnjs — one of the Internet's busiest open-source JavaScript/CSS CDNs (12% of all websites, 108K req/sec, 9 billion requests/day, 98.6% cache hit rate) — from a legacy architecture split across GCP Cloud Functions + a git-sync VM + Workers KV to run entirely on Cloudflare's Developer Platform: Workers, Workflows, R2, KV, Queues, Containers, Workers Cache, and Durable Objects. The migration eliminated five architectural pain points (no shared trace, split-brain storage, object-event-based pipeline glue, 26 sharded functions, and a 1.1 TB GitHub repo GitHub couldn't serve) and consolidated both serving and publishing into a single platform with durable execution semantics.
Key takeaways¶
-
R2 is the single source of truth — replaces the previous split-brain (Workers KV + GitHub repo). No size limits, so source maps and large bundles that couldn't fit in KV now live alongside everything else. The S3 API makes the entire catalog accessible to any S3 client. (Section: "How we re-built it")
-
KV stores only metadata now — package info, version lists, SRI hashes. KV's high-read/infrequent-write profile matches metadata access perfectly; file content moved entirely to R2. (Section: "How we re-built it")
-
Workers Cache replaces an internal caching layer — a tiered cache sits in front of the Worker, owned by the Developer Platform rather than a separate internal team. One less moving part. (Section: "How we re-built it")
-
DigitalOcean Spaces as disaster-recovery fallback — every file published to R2 is mirrored to DigitalOcean Spaces. Serving chain is
cache → R2 → DigitalOcean, so R2 having a bad day doesn't take cdnjs down. (Section: "How we re-built it") -
Cloudflare Workflows replaces the GCP Cloud Functions chain — a cron-triggered
PackageUpdatesWorkflowevery 10 minutes spawns per-versionDownloadPackageWorkflow→ per-fileProcessingWorkflow→PublishingWorkflow. Durable execution means each step's state is preserved and resumes from the last successful step on failure. (Section: "How we re-built it") -
Containers handle CPU-intensive compression — text-based files are pre-compressed (Brotli + gzip), but compression is too CPU-intensive for a Worker. A Rust compression service runs in Cloudflare Containers; Workflows hands off via a Queue, hibernates, and wakes on an R2 event notification when compression completes. (Section: "How we re-built it")
-
Durable Objects as a parent-child coordination counter — a package with thousands of files spawns thousands of child ProcessingWorkflows. A small Durable Object acts as a counter: parent increments on each spawn, children decrement on finish, parent wakes when counter hits zero. (Section: "How we re-built it")
-
Copy-not-regenerate migration strategy — a previous attempt to re-process old packages failed because minifiers/compressors aren't deterministic across versions, producing different SRI hashes. Since users pin SRI hashes in HTML, the team migrated existing KV content to R2 as-is rather than regenerating. (Section: "Pushing the limits")
-
Queue-sharded migration overcame subrequest limits — the 1,000 subrequest-per-invocation limit (at the time) was too low for packages with thousands of files. Work was fanned out via Queues with at-least-once delivery so no package could silently drop. (Section: "Pushing the limits")
-
Platform limits lifted for everyone — cdnjs hit the 1,000 subrequest limit and the 1,024 Workflow step limit. Rather than just working around them, Cloudflare raised both: subrequests now go to 10 million on paid plans; Workflows default to 10,000 steps (configurable to 25,000). (Section: "Pushing the limits")
Architecture & numbers¶
| Metric | Value |
|---|---|
| Requests/sec | 108,000 average |
| Requests/day | 9 billion |
| Website market share | 12% of all websites (48.3% JS CDN market) |
| Cache hit rate | 98.6% |
| Data centers | 330+ Cloudflare |
| Previous GitHub repo size | 1.1 TB packed |
| Previous pipeline shape | 26 GCP Cloud Functions (one per alphabet letter) + GCS object events + git-sync VM |
| New subrequest limit | 10 million (paid plans) |
| New Workflow step limit | 10,000 default / 25,000 configurable |
| Pipeline cadence | Every 10 minutes (cron-triggered) |
Extracted systems/concepts/patterns¶
Systems¶
- systems/cdnjs — free open-source JS/CSS CDN, now running entirely on Cloudflare's Developer Platform
- systems/cloudflare-workflows — durable execution for the ingestion pipeline
- systems/cloudflare-r2 — single source of truth for file content
- systems/cloudflare-kv — metadata-only store (package info, version lists, SRI hashes)
- systems/cloudflare-queues — fan-out for file processing and migration sharding
- systems/cloudflare-containers — CPU-intensive Rust compression service
- systems/cloudflare-durable-objects — parent-child workflow coordination counter
- systems/workers-cache — tiered cache in front of the serving Worker
- systems/digitalocean-spaces — DR fallback mirroring R2
Concepts¶
- concepts/dogfooding — running cdnjs on the same building blocks anyone can use
- concepts/split-brain-storage — the failure mode of files living in both KV and GitHub with no reconciliation
- concepts/durable-execution — Workflows preserving step state across failures
- concepts/sri-hash-immutability — why copy-not-regenerate was required (non-deterministic builds produce different hashes)
- concepts/platform-limit-lifting — raising limits encountered by internal customers for everyone
- concepts/at-least-once-delivery — Queues guaranteeing no package silently drops during migration
- concepts/correlation-id — the observability primitive the old architecture lacked (no shared trace)
- concepts/event-driven-pipeline — the antipattern of using storage object events as a message queue
Patterns¶
- patterns/workflows-for-ingestion-pipeline — replacing event-driven Cloud Function chains with durable Workflows
- patterns/r2-as-source-of-truth — object storage as the authoritative store (vs split KV + Git)
- patterns/kv-for-metadata-only — separating metadata (high-read, infrequent-write) from content
- patterns/container-for-cpu-intensive-work — offloading compression to Containers while Workflows hibernates
- patterns/durable-object-as-counter — DO as a coordination primitive for parent-child workflow fan-out
- patterns/r2-event-notification-wake — using R2 event notifications to wake a hibernating Workflow
- patterns/copy-not-regenerate-migration — migrating content as-is to preserve SRI hashes
- patterns/queue-sharded-migration — fanning migration work across Queues to overcome per-invocation limits
- patterns/tiered-cache-in-front-of-worker — Workers Cache eliminating a separate internal caching layer
Caveats¶
- No latency or performance numbers comparing old vs new architecture disclosed
- No failure-rate or incident-count comparison disclosed
- Specific Workflow / Queue / Container SKU sizing not disclosed
- Algolia search index update mechanism described only in passing
- DigitalOcean Spaces mirroring mechanism (sync vs async, lag) not detailed
- No per-file compression ratio or time-to-compress numbers
- "SRI hash" integrity verification described as still in-progress for legacy data
- Origin server still in chain during transition (pending GitHub backfill to R2)
- No disclosure of how many Workflow instances run concurrently at peak
- ESM module transformation explicitly future-looking ("not committing to it")
Source¶
- Original: https://blog.cloudflare.com/cdnjs-dev-platform-migration/
- Raw markdown:
raw/cloudflare/2026-07-30-dogfooding-at-scale-migrating-cdnjs-to-cloudflares-developer-8d228183.md
Related¶
- sources/2026-05-28-cloudflare-how-we-built-cloudflares-data-platform-and-an-ai-agent-on-top-of-it — sibling dogfood-the-platform-as-reference-architecture shape (data analytics workload)
- sources/2026-05-13-cloudflare-browser-run-now-running-on-cloudflare-containers-its-faster — sibling Containers customer-zero migration
- sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant — Workflows V2 capacity disclosure