cdnjs completes its move to Cloudflare’s Developer Platform
As of June 23, 2026, cdnjs — the open-source CDN used by roughly 12% of all websites — is running entirely on Cloudflare’s Developer Platform. The project serves an average of 108,000 requests per second (9 billion per day) across 330+ data centers with a 98.6% cache hit rate. The migration wasn't about fixing performance problems; it was about modernizing how the service is built and maintained.
cdnjs provides free, hash-verified mirrors of popular JavaScript and CSS libraries. Developers drop a <script> tag pointing to cdnjs.cloudflare.com and the library loads from the edge — no signup, no API keys, no rate limits. Fifteen years after its creation in 2011, it remains a staple of tutorials, demos, and Q&A threads. The platform is also a favorite of LLMs, which reliably generate cdnjs URLs from training data because the URL pattern is consistent and versions are immutable.
Why move a system that was working?
The previous serving architecture was solid: a 98% cache hit rate, billions of requests, and few outages. The pain was all on the publishing side. The pipeline that watched npm and GitHub for new library versions had grown organically since 2020, when cdnjs moved file serving to Workers and KV but left ingestion on Google Cloud Platform (GCP). At the time, Cloudflare Workers couldn't handle long-running, multi-step workflows — Workflows, Queues, Durable Objects, and R2 didn't exist.
The result was a five-part architecture held together by GCP Cloud Functions, a git-sync VM, and a GitHub repository as the source of truth. It worked, but it had accumulated serious friction:
- No shared trace. A single package update crossed Cloud Functions, GCS object events, Pub/Sub, a VM, and Workers KV. None shared a correlation ID, so debugging meant manually stitching logs from GCP Logging and Cloudflare Logpush. Partial failures — a version that wrote to KV but silently missed the GitHub repo — could go unnoticed for weeks.
- Split-brain storage. Files lived in both Workers KV and the GitHub repo, with neither authoritative. Reconciliation wasn't possible when they diverged.
- Object events as a message queue. Each Cloud Function handed off to the next via storage triggers, with no dead-letter queue, no backlog visibility, and no clean replay on failure.
- 26 functions for 26 letters. npm update checks were sharded alphabetically across 26 separate Cloud Functions, each with its own deployment and logs.
- An unmanageable GitHub repo. The cdnjs/cdnjs repository had grown past 1.1TB with a 274-entry
.gitignore, so large that GitHub's archive service refused to generate tarballs, and forks were impractical.
The migration also reduced the security surface: retiring the GCP pipeline eliminated Cloud Functions, a git-sync VM, container images, GCS buckets, and service-account keys.
The new architecture
R2 is now the single source of truth for file content. It has no practical size limit, which means source maps, large bundles, and font packs — previously too big for KV — now live alongside everything else. The S3 API also makes the full catalog accessible to any S3 client for external mirroring.
KV holds only metadata: package info, version lists, and SRI hashes. In front of the serving Worker is Workers Cache, a tiered cache launched this year, which replaces a separate internal caching layer between the edge and the Worker.
The new setup also extends a partnership with DigitalOcean, which already hosted the cdnjs website. Every file published to R2 is mirrored to DigitalOcean Spaces, which acts as both a disaster-recovery copy and a live fallback. The serving chain is cache → R2 → DigitalOcean, with a Cloudflare-hosted origin remaining only until the GitHub backfill lands in R2.
The ingestion pipeline runs on Cloudflare Workflows. Every ten minutes, a cron job triggers PackageUpdatesWorkflow, which checks npm and GitHub for new versions. For each new version, it spawns a DownloadPackageWorkflow that fetches the tarball into R2, then a ProcessingWorkflow per file that extracts, minifies, and compresses. Finally, PublishingWorkflow writes results to R2 and KV and updates the Algolia search index.
Workflows provide durable execution, so a failure at any step resumes from the last successful state. Gluing Workflows to the external compression container is the trickiest part. The library processing algorithms require buffering entire files in memory, so the initial migration uses a containerized Rust compression service. The team is investigating streaming algorithms to move this logic into Workers.
The pipeline handles two kinds of waiting:
- Per file: Each
ProcessingWorkflowwrites an uncompressed file to R2, sends a job to a Queue, and hibernates. A Rust compression container picks it up, compresses it, writes to another bucket, and an R2 event notification wakes the workflow. - Per package: A parent workflow with thousands of file children waits via a small Durable Object counter. The parent increments on spawn, children decrement on completion, and the parent wakes when the counter hits zero.
Pushing platform limits
An earlier attempt at this migration had to be rolled back. The plan then was to re-process old packages and write fresh results to R2, but regenerated files didn't byte-match what KV was serving — minifiers and compressors aren't deterministic across versions, so the outputs had different SRI hashes. For a CDN where users pin those hashes in HTML, that's a serving break. The team instead copied existing content from KV to R2 as-is.
That decision turned the problem into copying millions of files between accounts without missing any. A package with thousands of files would hit the Workers subrequest limit of 1,000 per invocation, and parallelizing didn't help because every Worker hits the same ceiling. The solution was sharding the migration by package name and fanning work out via Queues, whose at-least-once delivery guaranteed nothing slipped through.
The migration surfaced two hard platform limits: 1,000 subrequests per Worker invocation and 1,024 steps per Workflow. Rather than working around them, the Workers and Workflows teams raised both. Subrequests now go up to 10 million on paid plans, and Workflows default to 10,000 steps, configurable to 25,000.
What's next
The architecture opens a door it didn't before: serving modern, browser-native ES modules. The same Workflows-plus-Containers pattern that pre-compresses files today could transform packages into import-ready ESM on publish. The team isn't committing to it, but the possibility now exists where it didn't a year ago.



