Skip to main content
Cloudflare Workers is a serverless platform that runs your code at the edge, close to your users worldwide. Remix applications work natively on Workers thanks to their web standards-based API.

Prerequisites

  • A Cloudflare account
  • Wrangler CLI installed (npm install -g wrangler)
  • A Remix application ready to deploy

Project Setup

Initialize a Workers project:
wrangler init
Create a wrangler.toml configuration:
name = "remix-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[build]
command = "npm run build"

# Environment variables
[vars]
ENVIRONMENT = "production"

Worker Entry Point

Create your Workers entry point:
src/index.ts
import router from './router'

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      return await router.fetch(request)
    } catch (error) {
      return new Response('Internal Server Error', { status: 500 })
    }
  },
}

Router Configuration

Your router works the same as on Node.js:
router.ts
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  home: '/',
  api: {
    users: '/api/users',
  },
})

let router = createRouter()

router.map(routes, {
  actions: {
    home() {
      return new Response('Hello from the edge!')
    },
    api: {
      actions: {
        users() {
          return Response.json({ users: [] })
        },
      },
    },
  },
})

export default router

Development

Run locally with Wrangler:
wrangler dev

Deployment

Deploy to Cloudflare:
wrangler deploy

Environment Variables & Secrets

Set environment variables:
wrangler secret put DATABASE_URL
wrangler secret put SESSION_SECRET
Access in your worker:
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    let databaseUrl = env.DATABASE_URL
    let sessionSecret = env.SESSION_SECRET
    // Use in your application
  },
}

Durable Objects

For stateful applications, use Durable Objects:
export class Counter {
  state: DurableObjectState
  count: number = 0

  constructor(state: DurableObjectState) {
    this.state = state
  }

  async fetch(request: Request) {
    this.count++
    return Response.json({ count: this.count })
  }
}

KV Storage

Bind KV namespaces in wrangler.toml:
[[kv_namespaces]]
binding = "CACHE"
id = "your-namespace-id"
Use in your application:
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    await env.CACHE.put('key', 'value')
    let value = await env.CACHE.get('key')
    return Response.json({ value })
  },
}

D1 Database

For SQL databases, use Cloudflare D1:
[[d1_databases]]
binding = "DB"
database_name = "remix-db"
database_id = "your-database-id"
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    let result = await env.DB.prepare('SELECT * FROM users').all()
    return Response.json(result)
  },
}

Limitations

  • CPU time: 50ms on free tier, more on paid plans
  • No Node.js built-ins (use web standards)
  • Request size: 100MB max

Best Practices

  • Keep workers lightweight
  • Use KV for caching
  • Minimize database queries
  • Use Durable Objects for state
  • Leverage edge caching

Web Standards

Why Remix works perfectly on Workers

Fetch Router

Router API reference

Build docs developers (and LLMs) love