---
title: "Sampling"
description: "Two-tier sampling: head sampling drops noise by level, and tail sampling keeps an event once its outcome is known."
canonical_url: "https://www.evlog.dev/learn/sampling"
---
# Sampling

> Two-tier sampling: head sampling drops noise by level, and tail sampling keeps an event once its outcome is known.

At scale, logging everything gets expensive fast. Sampling lets you keep costs under control without losing visibility into what matters. evlog uses a two-tier approach: head sampling drops noise upfront, tail sampling rescues critical events after the fact.

<prompt :actions="["copy","cursor","claude"]" description="Enable head and tail sampling" icon="i-lucide-filter">

Enable head and tail sampling in my evlog production config.

- Identify my framework and locate the evlog config (nuxt.config.ts, lib/evlog.ts, initLogger, etc.)
- Configure sampling.rates per level: { info: 10, warn: 50, debug: 0, error: 100 } as a starting point
- Add sampling.keep rules to force-keep critical events: <span>

{ status: 400 }, { duration: 1000 }, { path: '/api/critical/**' }

</span>
- For business-specific keep logic (e.g. premium users), add a custom keep callback or evlog:emit:keep hook
- Wrap sampling in a $production override so dev keeps full logging
- Confirm errors are always kept by default unless I explicitly set error: 0

Docs: [https://www.evlog.dev/learn/sampling](https://www.evlog.dev/learn/sampling)
Best practices: [https://www.evlog.dev/reference/best-practices](https://www.evlog.dev/reference/best-practices)

</prompt>

## Drop noise before it is built

Head sampling randomly keeps a percentage of logs per level. It runs **before** the request completes, acting as a coin flip at emission time.

<head-sampling-plinko>



</head-sampling-plinko>

<framework-tabs>

```typescript [Nuxt]nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    sampling: {
      rates: {
        info: 10,    // Keep 10% of info logs
        warn: 50,    // Keep 50% of warnings
        debug: 0,    // Drop all debug logs
        error: 100,  // Always keep errors (default)
      },
    },
  },
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'

export const { withEvlog, useLogger } = createEvlog({
  service: 'my-app',
  sampling: {
    rates: {
      info: 10,
      warn: 50,
      debug: 0,
      error: 100,
    },
  },
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'

initLogger({
  env: { service: 'my-app' },
  sampling: {
    rates: {
      info: 10,
      warn: 50,
      debug: 0,
      error: 100,
    },
  },
})
```

</framework-tabs>

Each level is a percentage from 0 to 100. Levels you don't configure default to 100% (keep everything). Error defaults to 100% even when other levels are configured, so you have to explicitly set `error: 0` to drop errors.

The same rates apply to browser logs received through the [client ingest endpoint](https://www.evlog.dev/integrate/frameworks/nuxt#client-transport): a client event sampled out is dropped before enrichment and draining. Tail sampling does not apply to client events, since they carry no status or duration.

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

Head sampling is random. A `10%` rate means roughly 1 in 10 info logs are kept, not exactly 1 in 10.

</callout>

## Keep an event once you know its outcome

Head sampling is blind: it doesn't know if a request was slow, failed, or hit a critical path. Tail sampling fixes this by evaluating **after** the request completes and force-keeping logs that match specific conditions.

```typescript [nuxt.config.ts]
// Sampling config, works the same across all frameworks
evlog: {
  sampling: {
    rates: { info: 10 },
    keep: [
      { status: 400 },              // HTTP status >= 400
      { duration: 1000 },           // Request took >= 1s
      { path: '/api/payments/**' }, // Critical path (glob)
    ],
  },
}
```

Conditions use **>=** comparison for `status` and `duration`, and glob matching for `path`. If **any** condition matches, the log is kept regardless of head sampling (OR logic).

### Conditions you can match on

<table>
<thead>
  <tr>
    <th>
      Condition
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Keep if HTTP status >= value (e.g., <code>
        400
      </code>
      
       catches all 4xx and 5xx)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        duration
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Keep if request duration >= value in milliseconds
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        path
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Keep if request path matches glob pattern (e.g., <code>
        '/api/critical/**'
      </code>
      
      )
    </td>
  </tr>
</tbody>
</table>

## Combine head and tail sampling

The two tiers complement each other:

1. **Request completes** - evlog knows the status, duration, and path
2. **Tail sampling evaluates** - if any `keep` condition matches, the log is force-kept
3. **Head sampling applies** - only if tail sampling didn't force-keep, the random percentage check runs
4. **Log emits or drops** - kept logs go through enrichment and draining as normal

This means a request to `/api/payments/charge` that returns a 500 in 2 seconds will always be logged, even if `info` is set to 1%. The tail conditions rescue it.

<tail-sample-decision>



</tail-sample-decision>

<code-group>

```typescript [Configuration]
sampling: {
  rates: { info: 10 },
  keep: [
    { status: 400 },
    { duration: 1000 },
  ],
}
```

```bash [What gets logged]
POST /api/users     200  45ms   → 10% chance (head sampling)
POST /api/users     500  45ms   → always kept (status >= 400)
GET  /api/products  200  2300ms → always kept (duration >= 1000)
POST /api/checkout  200  120ms  → 10% chance (head sampling)
```

</code-group>

## Write your own keep rule

For conditions beyond status, duration, and path, use the `evlog:emit:keep` hook in Nuxt/Nitro or the `keep` callback in other frameworks.

<framework-tabs>

```typescript [Nuxt]server/plugins/sampling.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  })
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'

export const { withEvlog, useLogger } = createEvlog({
  service: 'my-app',
  sampling: {
    rates: { info: 10 },
    keep: [{ status: 400 }],
  },
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'

export const { handle, handleError } = createEvlogHooks({
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
})
```

```typescript [TanStack Start]server/plugins/evlog.ts
import { definePlugin } from 'nitro'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  })
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
    keep(ctx) {
      if (ctx.context.user?.plan === 'enterprise') {
        ctx.shouldKeep = true
      }
    },
  }),
]
```

```typescript [Standalone]src/index.ts
import { evlog } from 'evlog/hono'

app.use(evlog({
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
}))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'

app.use(evlog({
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
}))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'

await app.register(evlog, {
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
})
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'

app.use(evlog({
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
}))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'

EvlogModule.forRoot({
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
})
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'

const handler = withEvlog(new RPCHandler(router), {
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
})
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'

export default withEvlog(handler, {
  keep(ctx) {
    if (ctx.context.user?.plan === 'enterprise') {
      ctx.shouldKeep = true
    }
  },
})
```

</framework-tabs>

The `ctx` object contains:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        number | undefined
      </code>
    </td>
    
    <td>
      HTTP response status
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        duration
      </code>
    </td>
    
    <td>
      <code>
        number | undefined
      </code>
    </td>
    
    <td>
      Request duration in ms
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        path
      </code>
    </td>
    
    <td>
      <code>
        string | undefined
      </code>
    </td>
    
    <td>
      Request path
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        method
      </code>
    </td>
    
    <td>
      <code>
        string | undefined
      </code>
    </td>
    
    <td>
      HTTP method
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        context
      </code>
    </td>
    
    <td>
      <code>
        Record<string, unknown>
      </code>
    </td>
    
    <td>
      All fields set via <code>
        log.set()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        shouldKeep
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Set to <code>
        true
      </code>
      
       to force-keep
    </td>
  </tr>
</tbody>
</table>

## A configuration that ships

A typical production configuration that balances cost and visibility:

<framework-tabs>

```typescript [Nuxt]nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    env: { service: 'my-app' },
  },
  $production: {
    evlog: {
      sampling: {
        rates: {
          info: 10,
          warn: 50,
          debug: 0,
          error: 100,
        },
        keep: [
          { status: 400 },
          { duration: 1000 },
          { path: '/api/payments/**' },
          { path: '/api/auth/**' },
        ],
      },
    },
  },
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'

export const { withEvlog, useLogger } = createEvlog({
  service: 'my-app',
  sampling: {
    rates: {
      info: 10,
      warn: 50,
      debug: 0,
      error: 100,
    },
    keep: [
      { status: 400 },
      { duration: 1000 },
      { path: '/api/payments/**' },
      { path: '/api/auth/**' },
    ],
  },
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'

initLogger({
  env: { service: 'my-app' },
  sampling: {
    rates: {
      info: 10,
      warn: 50,
      debug: 0,
      error: 100,
    },
    keep: [
      { status: 400 },
      { duration: 1000 },
      { path: '/api/payments/**' },
      { path: '/api/auth/**' },
    ],
  },
})
```

</framework-tabs>

<callout color="warning" icon="i-lucide-lightbulb">

In Nuxt, use the `$production` override to keep full logging in development while sampling in production. In other frameworks, use your own environment check or config system.

</callout>

## Next Steps

- [Best Practices](https://www.evlog.dev/reference/best-practices) - Security and production checklist
- [Wide Events](https://www.evlog.dev/learn/wide-events) - Design effective wide events

---

- [Best Practices](https://www.evlog.dev/reference/best-practices)
- [Wide Events](https://www.evlog.dev/learn/wide-events)


## Sitemap

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