---
title: "Cloudflare Workers"
description: "Wide events, structured errors, and logging in Cloudflare Workers and Durable Objects."
canonical_url: "https://www.evlog.dev/integrate/frameworks/cloudflare-workers"
---
# Cloudflare Workers

> Wide events, structured errors, and logging in Cloudflare Workers and Durable Objects.

The `evlog/workers` adapter instruments Cloudflare Workers and Durable Objects with request-scoped loggers carrying Cloudflare-specific context.

Use **withEvlog** to get the same middleware pipeline as every other framework integration: route filtering, redaction, enrich, tail sampling, plugins, drains, and automatic emit. Reach for `defineWorkerFetch` or `createWorkersLogger` when you'd rather own the emit yourself.

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my Cloudflare Worker" icon="i-simple-icons-cloudflare">

Set up evlog in my Cloudflare Worker.

- Install evlog: pnpm add evlog
- Import initWorkersLogger and withEvlog from 'evlog/workers'
- Create evlog.config.ts with export default defineEvlog({ service: 'my-worker' })
- Call initWorkersLogger(toLoggerConfig(config)) at the top level and pass toMiddlewareOptions(config) to withEvlog
- Wrap the fetch handler with **withEvlog** (recommended). It emits the wide event for you and accepts include/exclude, routes, redact, enrich, keep, plugins and drain
- Use log.set() to accumulate context throughout the request
- Only use defineWorkerFetch or createWorkersLogger if you want to call log.emit() yourself

Docs: [https://www.evlog.dev/integrate/frameworks/cloudflare-workers](https://www.evlog.dev/integrate/frameworks/cloudflare-workers)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog
```

```bash [bun]
bun add evlog
```

```bash [yarn]
yarn add evlog
```

```bash [npm]
npm install evlog
```

</code-group>

### 2. Create evlog.config.ts

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-worker',
})
```

Every option lives in this file: sampling, drain, `enrich`, route filters. `npx evlog init` writes it, and the wiring below, for you. See [Configuration](https://www.evlog.dev/reference/configuration).

### 3. Wrap your fetch handler

```typescript [src/worker.ts]
import { toLoggerConfig, toMiddlewareOptions } from 'evlog'
import { initWorkersLogger, withEvlog } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

export default withEvlog(async (request, _env, _ctx, log) => {
  log.set({ action: 'handle_request' })

  // ... your handler logic

  return Response.json({ ok: true })
}, toMiddlewareOptions(config))
```

`withEvlog` emits one wide event per request when the handler returns, with no manual `log.emit()`. It reads `ExecutionContext` off the third argument, so async **drain** calls (PostHog, Axiom, …) are registered with `waitUntil` and stay alive after the response is returned. Streaming responses defer the emit until the body completes.

`requestId` comes from `x-request-id` when the caller sends one, falling back to `cf-ray`. `method`, `path`, `cf-ray`, `traceparent` and the safe subset of `request.cf` are captured automatically.

### Options

`withEvlog` takes the same options as every other framework integration. Put them in the file:

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'my-worker',
  drain: createAxiomDrain(),
  exclude: ['/health'],
  routes: { '/api/**': { service: 'api' } },
  redact: true,
  enrich: (ctx) => {
    ctx.event.colo = ctx.event.colo ?? 'unknown'
  },
  keep: (ctx) => {
    if (ctx.duration > 1000) ctx.shouldKeep = true
  },
})
```

`initWorkersLogger` always sets `pretty` and `stringify` to `false`, whatever the file says. The file is bundled into the Worker and evaluated there, so a `process.env` read in it only sees your Worker's variables when Cloudflare exposes them on `process.env` (the `nodejs_compat` flag). Adapters such as `createAxiomDrain()` read their keys the same way.

### Emitting manually

Prefer to own the emit? `defineWorkerFetch` wires `ExecutionContext` for you but leaves `log.emit()` to you:

```typescript [src/worker.ts]
import { toLoggerConfig } from 'evlog'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

export default defineWorkerFetch(async (request, _env, _ctx, log) => {
  log.set({ action: 'handle_request' })
  log.emit()
  return Response.json({ ok: true })
})
```

<callout color="info" icon="i-lucide-info">

`defineWorkerFetch` and `createWorkersLogger` are the low-level path: they create the logger and leave the lifecycle to you, so `include` / `exclude`, `routes`, `redact`, `enrich`, `keep` and `plugins` do **not** apply. Use `withEvlog` to get them.

</callout>

## Wide Events

Build up context progressively, then emit at the end:

```typescript [src/worker.ts]
import { toLoggerConfig } from 'evlog'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

export default defineWorkerFetch(async (request, env, _ctx, log) => {
  const url = new URL(request.url)

  log.set({ route: url.pathname })

  const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(url.searchParams.get('userId')).first()
  log.set({ user: { id: user.id, plan: user.plan } })

  const orders = await env.DB.prepare('SELECT COUNT(*) as count FROM orders WHERE user_id = ?').bind(user.id).first()
  log.set({ orders: { count: orders.count } })

  log.emit()
  return Response.json({ user, orders })
})
```

```bash [Terminal output]
14:58:15 INFO [my-worker] GET /api/users 200 in 12ms
  ├─ orders: count=5
  ├─ user: id=usr_123 plan=pro
  ├─ route: /api/users
  └─ requestId: 4a8ff3a8-...
```

## Error Handling

Use `createError` for structured errors and handle them with try/catch:

<code-collapse>

```typescript [src/worker.ts]
import { createError, parseError } from 'evlog'
import { toLoggerConfig } from 'evlog'
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
import config from '../evlog.config'

initWorkersLogger(toLoggerConfig(config))

export default defineWorkerFetch(async (request, env, _ctx, log) => {
  try {
    const body = await request.json()
    log.set({ payment: { amount: body.amount } })

    if (body.amount <= 0) {
      throw createError({
        status: 400,
        message: 'Invalid payment amount',
        why: 'The amount must be a positive number',
        fix: 'Pass a positive integer in cents',
      })
    }

    log.emit()
    return Response.json({ success: true })
  } catch (error) {
    log.error(error instanceof Error ? error : new Error(String(error)))
    log.emit()

    const parsed = parseError(error)
    return Response.json({
      message: parsed.message,
      why: parsed.why,
      fix: parsed.fix,
    }, { status: parsed.status })
  }
})
```

</code-collapse>

## Configuration

Every option goes in `evlog.config.ts`. See the [Configuration reference](https://www.evlog.dev/reference/configuration) for the full list.

## Drain & Enrichers

Put the drain and the enrichers in the file. `toLoggerConfig` hands the drain to `initWorkersLogger`, and `toMiddlewareOptions` hands both to `withEvlog`, which runs them on every request event:

```typescript [evlog.config.ts]
import { defineEvlog, type DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createUserAgentEnricher } from 'evlog/enrichers'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
})
const userAgent = createUserAgentEnricher()

export default defineEvlog({
  service: 'my-worker',
  drain: pipeline(createAxiomDrain()),
  enrich: (ctx) => {
    userAgent(ctx)
  },
})
```

<callout color="info" icon="i-lucide-info">

See the [Adapters](https://www.evlog.dev/integrate/adapters/overview) and [Enrichers](https://www.evlog.dev/use-cases/enrichers) docs for all available drain adapters and enrichers.

</callout>

## Wrangler Configuration

Disable Cloudflare's default invocation logs to avoid duplicates when using evlog:

```toml [wrangler.toml]
[observability]
enabled = false
```

## Run Locally

```bash [Terminal]
wrangler dev
```

## Next Steps

- [Wide Events](https://www.evlog.dev/learn/wide-events): Design comprehensive events with context layering
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): Send logs to Axiom, Sentry, PostHog, and more
- [Sampling](https://www.evlog.dev/learn/sampling): Control log volume with head and tail sampling
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Throw errors with `why`, `fix`, and `link` fields


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
