---
title: "How to set up Cloudflare Workers"
slug: "set-up-cloudflare-workers"
category: "howto"
tags: ["howto", "cloudflare", "workers", "kv", "tutorial", "ops", "edge"]
status: "stable"
last_updated: 2026-05-14
summary: "Initialize a Worker with wrangler, write a fetch handler, bind KV storage, deploy to production, and verify with a curl request."
related: ["[[ops/cloudflare-workers]]", "[[ops/cloudflare-kv]]", "[[ops/cloudflare]]", "[[ops/cloudflare-durable-objects]]", "[[comparisons/vercel-vs-cloudflare]]", "[[howto/set-up-cloudflare-r2]]", "[[howto/set-up-cloudflare-tunnel]]", "[[tooling/github-actions]]"]
---

## Overview

Cloudflare Workers run JavaScript (or WASM) at the edge, globally, with zero cold starts. A Worker handles HTTP requests in a `fetch` handler, optionally reading from KV for low-latency key-value lookups. The broader platform reference lives in [[ops/cloudflare-workers]]; KV specifics are in [[ops/cloudflare-kv]].

## Prerequisites

- A Cloudflare account. Free tier supports Workers with 100,000 daily requests.
- Node 20+ and npm on the path.
- Wrangler installed globally: `npm install -g wrangler`.
- Authenticated: `wrangler login` opens the browser OAuth flow.

## Steps

### 1. Initialize the project

```bash
npm create cloudflare@latest my-worker
cd my-worker
```

Choose "Hello World" when prompted for a template. This generates `wrangler.toml`, `src/index.ts`, and a minimal `tsconfig.json`.

Key fields in `wrangler.toml`:

```toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2025-01-01"
```

Set `compatibility_date` to a recent date to opt into the latest runtime behaviour flags. Do not leave it at the scaffold default from years ago.

### 2. Write the fetch handler

Every Worker exports a `fetch` function that receives a `Request` and returns a `Response`.

```typescript
// src/index.ts
export interface Env {
  MY_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return new Response("ok", { status: 200 });
    }

    const key = url.searchParams.get("key");
    if (!key) {
      return new Response("Missing key param", { status: 400 });
    }

    const value = await env.MY_KV.get(key);
    if (!value) {
      return new Response("Not found", { status: 404 });
    }

    return Response.json({ key, value });
  },
};
```

Type the `Env` interface to match the bindings declared in `wrangler.toml`. The TypeScript compiler catches missing bindings before deployment.

### 3. Bind KV storage

Create a KV namespace in the Cloudflare dashboard or via wrangler:

```bash
wrangler kv namespace create MY_KV
# Outputs: id = "abc123..."

wrangler kv namespace create MY_KV --preview
# Outputs: preview_id = "def456..."
```

Add the binding to `wrangler.toml`:

```toml
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123..."
preview_id = "def456..."
```

The `binding` value matches the key in the `Env` interface. The `preview_id` is used during local development. See [[ops/cloudflare-kv]] for TTL and metadata patterns.

### 4. Test locally

```bash
wrangler dev
# Worker running at http://localhost:8787
```

In another terminal:

```bash
# Write a value to the preview namespace
wrangler kv key put --namespace-id=def456... "hello" "world"

# Test the endpoint
curl "http://localhost:8787/?key=hello"
# {"key":"hello","value":"world"}
```

`wrangler dev` hot-reloads on file changes. The local runtime matches the production runtime closely; most bugs surface here before deployment.

### 5. Deploy to production

```bash
wrangler deploy
# Uploading my-worker...
# Published my-worker (1.23 sec)
# https://my-worker.your-subdomain.workers.dev
```

The worker is live globally within seconds. Verify with curl:

```bash
curl "https://my-worker.your-subdomain.workers.dev/health"
# ok
```

To add a custom domain, configure a route in `wrangler.toml`:

```toml
routes = [
  { pattern = "api.example.com/*", zone_name = "example.com" }
]
```

See [[ops/cloudflare]] for DNS setup and [[comparisons/vercel-vs-cloudflare]] for when Workers is the better choice over Vercel Edge Functions.

### 6. Wire to CI for automated deploys

Add a GitHub Actions step to deploy on push to `main`. Store `CLOUDFLARE_API_TOKEN` in GitHub secrets. See [[tooling/github-actions]] for the full workflow pattern.

```yaml
- name: Deploy Worker
  run: wrangler deploy --env production
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
```

## Verify it worked

```bash
# Health check
curl -si "https://my-worker.your-subdomain.workers.dev/health" | head -5
# HTTP/2 200

# KV round-trip
wrangler kv key put --namespace-id=abc123... "ping" "pong"
curl "https://my-worker.your-subdomain.workers.dev/?key=ping"
# {"key":"ping","value":"pong"}

# Tail live logs
wrangler tail my-worker
```

## Common errors

- **`Error: No account id found`**. Add `account_id = "..."` to `wrangler.toml` or set `CLOUDFLARE_ACCOUNT_ID` in the environment.
- **KV binding returns `undefined`**. The `binding` name in `wrangler.toml` does not match the field name in `Env`. Both must be identical.
- **`compatibility_date` warning**. The date in `wrangler.toml` is in the past; update it to within the last 12 months to use current runtime flags.
- **Deploy fails with "script too large"**. Workers have a 1 MB compressed script limit. Use dynamic imports or move heavy logic to a Durable Object; see [[ops/cloudflare-durable-objects]].
- **Local `wrangler dev` differs from production**. Some APIs (like `crypto.subtle`) behave differently in the local runtime. Test with `wrangler dev --remote` to run against the real edge infrastructure.

## Related

- [[ops/cloudflare-workers]]
- [[ops/cloudflare-kv]]
- [[ops/cloudflare]]
- [[ops/cloudflare-durable-objects]]
- [[comparisons/vercel-vs-cloudflare]]
- [[howto/set-up-cloudflare-r2]]
- [[howto/set-up-cloudflare-tunnel]]
- [[tooling/github-actions]]
