- Published on
Serve Static Files in a Cloudflare Worker (2025 Guide)
- Authors
- Name
- Ahmed Farid
- @
[!INFO] Workers now push up to 25 MB of static assets in a single module thanks to the 2025 module-worker size bump.
We’ll explore three approaches to host static content from a Worker:
- Bundled assets via
wrangler.toml
assets
→ simplest for sites <25 MB. - KV namespace for cacheable key/value storage.
- R2 bucket for large binaries (videos, PDFs).
By the end you’ll ship a micro-site under 1 ms cold-start latency globally.
Table of Contents
- Table of Contents
- 1. Prerequisites
- 2. Quick Start: Bundled Assets (<25 MB)
- 3. Medium Sites: KV Namespace (≤1 GiB)
- 4. Large Files: Serve from R2
- 5. Route to Custom Domain
- 6. Security & Best Practices
- 7. Automated CI with GitHub Actions
- 8. Conclusion
1. Prerequisites
- Node 20 + Wrangler 3.9.
- Cloudflare account with Workers, KV, R2 beta enabled.
2. Quick Start: Bundled Assets (<25 MB)
2.1 Create Project
npm create cloudflare@latest static-worker
cd static-worker
Choose TypeScript and yes to Include a sample static asset directory.
2.2 Directory Layout
static-worker/
public/
index.html
css/style.css
img/logo.webp
src/
index.ts
wrangler.toml
2.3 Wrangler Config
wrangler.toml
:
name = "static-worker"
main = "src/index.ts"
assets = "public"
compatibility_date = "2025-07-30"
The assets
field instructs Wrangler to bundle every file under public/
and map to ASSETS
binding.
2.4 Worker Code
src/index.ts
:
import type { Env } from './types'
import { getAssetFromKV } from '@cloudflare/kv-asset-handler'
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
try {
// Attempt to find asset in bundle
return await getAssetFromKV({ request: req, waitUntil: ctx.waitUntil.bind(ctx) })
} catch (err: any) {
if (err.status === 404) return new Response('Not Found', { status: 404 })
return new Response('Internal Error', { status: 500 })
}
},
}
⚡ Perf: Assets are cached in edge memory automatically. You pay only CPU time for misses.
2.5 Deploy
wrangler deploy
Your site is live at https://static-worker.your-subdomain.workers.dev
.
3. Medium Sites: KV Namespace (≤1 GiB)
3.1 Provision KV
wrangler kv:namespace create STATIC_KV --preview
Add binding to wrangler.toml
:
[[kv_namespaces]]
binding = "STATIC_KV"
id = "<NAMESPACE_ID>"
preview_id = "<PREVIEW_ID>"
3.2 Upload Assets
npx wrangler kv:bulk put STATIC_KV ./public
3.3 Worker Fetch Logic
export default {
async fetch(req: Request, env: Env) {
const url = new URL(req.url)
let key = url.pathname === '/' ? '/index.html' : url.pathname
const object = await env.STATIC_KV.get(key, { type: 'arrayBuffer', cacheTtl: 60 * 60 })
if (!object) return new Response('Not found', { status: 404 })
return new Response(object, {
headers: { 'Content-Type': getMime(key), 'Cache-Control': 'public,max-age=31536000' },
})
},
}
getMime
can be a small map (text/html
, image/webp
, …) or use mime-types
lib.
4. Large Files: Serve from R2
4.1 Create Bucket
wrangler r2 bucket create static-files
4.2 Upload
wrangler r2 object put static-files/img/hero.jpg --file=./public/img/hero.jpg
Add binding:
[[r2_buckets]]
binding = "STATIC_R2"
bucket_name = "static-files"
4.3 Worker Handler (Streaming)
export default {
async fetch(req: Request, env: Env) {
const key = new URL(req.url).pathname.slice(1) || 'index.html'
const obj = await env.STATIC_R2.get(key)
if (!obj) return new Response('404', { status: 404 })
const headers = new Headers()
headers.set('Content-Type', getMime(key))
headers.set('Cache-Control', 'public, max-age=604800')
return new Response(obj.body, { headers })
},
}
R2 streaming avoids pulling entire file into memory, suitable for 100 MB videos.
5. Route to Custom Domain
In Cloudflare Dashboard → Workers Routes → example.com/*
map to static-worker
.
6. Security & Best Practices
- Enable Cache Reserve for rarely-changing assets.
- Use Content-Security-Policy headers.
- Compress at build time (brotli) → Workers auto-adds
content-encoding
.
7. Automated CI with GitHub Actions
.github/workflows/deploy.yml
:
name: Deploy
on: [push]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with: { bun-version: latest }
- run: bun install
- run: bun run build # if using bundler
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
command: wrangler deploy --minify
8. Conclusion
Cloudflare Workers let you host static sites, SPAs, or asset CDNs with near-zero latency and cost. Pick bundled assets for tiny sites, KV for medium, or R2 for anything bigger—and sleep tight knowing your content is globally distributed. 🌐🚀