{ }
Published on

Serve Static Files in a Cloudflare Worker (2025 Guide)

Authors
  • avatar
    Name
    Ahmed Farid
    Twitter
    @

[!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:

  1. Bundled assets via wrangler.toml assets → simplest for sites <25 MB.
  2. KV namespace for cacheable key/value storage.
  3. 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

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 Routesexample.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. 🌐🚀