---
title: "Cloudflare"
description: "Durable Objects for reliability and Analytics Engine for the audit warehouse — how they fit together."
---

> Documentation Index
> Fetch the complete documentation index at: https://logbun-docs.abshahin.workers.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare

Logbun on Workers has **two separate pieces**. Keep them straight:

- **Reliability — `CloudflareReliabilityAdapter`** (`logbun/durability/cloudflare`, **ESM-only**). Lives **inside a Durable Object**. Owns the SQLite journal and DLQ (`{prefix}_journal`, `{prefix}_dlq`), recovers orphans, and schedules `alarm()` for retries and retention. It does **not** store queryable audit rows and does **not** use D1, R2, KV, or Node `fs`.
- **Destination — `CloudflareAnalyticsEngineAdapter`** (`logbun/adapters/cloudflare-analytics-engine`, ESM + CJS). Lives wherever you construct it (usually **inside the same DO** so the AE binding is in scope). Owns the Analytics Engine dataset via `writeDataPoint` and queries it via the SQL API. It does **not** provide a journal — without the DO adapter, `fire()` is volatile.

For durable Workers you **pair them**: the DO is the durable admission log, AE is the queryable warehouse. Workers call the DO; the DO appends to its journal and writes to AE.

> AE is an analytics sink, not a compliance store. Ingest is asynchronous and may be sampled, retention is ~3 months, and `writeDataPoint` is fire-and-forget. The DO journal is what survives process death.

## Wiring them together

The Durable Object owns both. `Env` carries the AE binding into the DO:

```ts title="audit-do.ts"
import { AuditLogger } from 'logbun';
import { CloudflareReliabilityAdapter } from 'logbun/durability/cloudflare';
import {
  CloudflareAnalyticsEngineAdapter,
  type AnalyticsEngineDatasetLike,
} from 'logbun/adapters/cloudflare-analytics-engine';

interface Env {
  AUDIT_AE: AnalyticsEngineDatasetLike; // analytics_engine_datasets binding
  ACCOUNT_ID?: string; // optional — enables AuditLogger.query()
  AE_API_TOKEN?: string; // optional — secret, Account Analytics Read
}

export class AuditDO {
  private audit: AuditLogger;

  constructor(private ctx: DurableObjectState, private env: Env) {
this.audit = new AuditLogger({
  namespace: 'do',
  mode: 'durable',
  reliability: new CloudflareReliabilityAdapter({
    state: ctx,                 // DO storage.sql required
    tablePrefix: 'logbun',      // {prefix}_journal / {prefix}_dlq
    maxJournalEntries: 100_000, // wal_full
    maxDlqEntries: 10_000,      // dlq_full
    scheduleAlarms: true,
    alarmDelayMs: 1_000,
  }),
  adapter: new CloudflareAnalyticsEngineAdapter({
    binding: env.AUDIT_AE,       // writeDataPoint (bulkInsert)
    dataset: 'logbun_audit',     // SQL dataset (query)
    accountId: env.ACCOUNT_ID,   // optional — query
    apiToken: env.AE_API_TOKEN,  // optional — query
  }),
});
  }

  async alarm() {
await this.audit.runMaintenance();
  }
}
```

### Wrangler

```jsonc title="wrangler.jsonc"
{
  "analytics_engine_datasets": [
{ "binding": "AUDIT_AE", "dataset": "logbun_audit" }
  ]
}
```

The `dataset` string must match in `wrangler.jsonc` and in the adapter constructor and must match `^[A-Za-z_][A-Za-z0-9_]{0,63}$`. `ACCOUNT_ID` is a plain var, `AE_API_TOKEN` is a secret with **Account Analytics Read**. Both are optional — without them the adapter is **write-only** and `AuditLogger.query()` throws `dataset, accountId, and apiToken are required for query`. `query` always adds `WHERE timestamp >= NOW() - INTERVAL '91' DAY` and dedupes by `id`.

Standard Workers should call a DO binding. Do not treat isolate-scoped volatile `fire()` as durable.

## Destination details — Analytics Engine

Full slot map, limits, and helpers live in [Adapters — CloudflareAnalyticsEngineAdapter](/reference/adapters#cloudflareanalyticsengineadapter):

- Shared dataset only — no `adapterFactory` / `database_per_tenant`.
- ≤250 `writeDataPoint` calls per invocation (bulkInsert throws **before** any write; hosts must keep `maxRecoveryBatch` ≤250 — the default batcher `maxSize` 100 already is).
- Index ≤96 bytes, total blobs ≤16 KiB per point — oversized points throw on write.
- `prune()` is a no-op (platform TTL ~3 months). `query` collapses duplicate `id`s from WAL/DLQ replay.

## Reliability details — Durable Object

These apply to `CloudflareReliabilityAdapter`, not to AE:

`tablePrefix` is a sanitized SQL identifier: `^[A-Za-z_][A-Za-z0-9_]{0,63}$`. Default `logbun`.

`init()` recovers DLQ orphans and requests a pending-work alarm so an isolate restart does not lose the wake-up. `runMaintenance()` is single-flight (plus one trailing pass). Let `alarm()` **propagate** rejection so the platform can retry. The adapter first restores a pending-work alarm after a failed maintenance phase.

## Admission vs scheduling

Journal append / DLQ write then call `requestMaintenance()`. If `getAlarm` / `setAlarm` fails after the row is committed, callers see `DurableAdmissionSchedulingError` with `durableAdmissionCommitted === true`. Do not resubmit that event.

```ts
import { isDurableAdmissionSchedulingError } from 'logbun';

try {
  await audit.fireAsync('audit.event', input);
} catch (error) {
  if (isDurableAdmissionSchedulingError(error)) {
await reliability.requestMaintenance();
  } else {
throw error;
  }
}
```

Use `fireAsync` + journal for admission that survives request end.

## `waitUntil`

Hono middleware reads `c.executionCtx.waitUntil` structurally when present and wraps it so host throws cannot escape `fire()`. Elysia does **not** inject `ExecutionContext` — pass `getWaitUntil`. See [Hono](/guides/hono) and [Elysia](/guides/elysia).

Source: https://logbun-docs.abshahin.workers.dev/guides/cloudflare/index.mdx
