Cloudflare Workers
Contents
This doc focuses on features unique to Cloudflare Workers. If you're looking for details on frameworks like React, Vue, Remix, Svelte, Hono, Astro, and more see their framework-specific docs.
Installation
To use PostHog in Cloudflare Workers, start by installing the posthog-node library:
Afterwards, set up your project token and host in your wrangler.jsonc (or wrangler.toml) file like this:
Next, set up the PostHog helper to create a PostHog client. We set flushAt to 1 and flushInterval to 0 to send captured data without batching. Batched data is sent asynchronously and Cloudflare Workers can terminate before it's sent causing data loss.
Usage
With PostHog installed, you can add and use it in your app like this:
You'll notice two different usage patterns here:
- We use
ctx.waitUntil()withcaptureImmediate()to capture an event. This doesn't block the response, but does ensure data is captured before the worker shuts down. - We
awaitflags which blocks the response until we get the feature flag data we need.
Error tracking
You can capture errors in Cloudflare Workers like you would in other Node applications using posthog.captureException().
For more details and a method for automatically capturing errors, see our error tracking installation docs.
Node.js compatibility
posthog-node ships a dedicated workerd export that avoids Node.js built-ins — it does not require nodejs_compat on its own. However, other dependencies in your project may need it. If you see missing-module errors at import time, add the compatibility flag to your Wrangler config:
Environment variables
Workers historically had no process.env, but since April 2025 (compatibility date 2025-04-01+), process.env is populated automatically when using the nodejs_compat flag. You can also use import { env } from 'cloudflare:workers' to access bindings from anywhere, including top-level scope.
How you access environment variables depends on your framework:
| Framework | Server-side env access |
|---|---|
| React Router 7 | context.cloudflare.env.VAR_NAME (in loaders, actions, middleware) |
| SvelteKit | platform.env.VAR_NAME (in hooks and server routes) |
| Nuxt | process.env works via unenv polyfill — but only inside event handlers, not at top-level. Prefer useRuntimeConfig(event) |
| Astro 5 | Astro.locals.runtime.env.VAR_NAME (in SSR pages and API routes) |
| Astro 6+ | import { env } from 'cloudflare:workers' (direct import; Astro.locals.runtime was removed) |
| Hono | env.VAR_NAME from the fetch handler's env parameter |
| Raw Workers | env.VAR_NAME from the fetch handler's env parameter |
Define variables in wrangler.toml under [vars] (non-secret) or via wrangler secret put (secret).
Event flushing
Workers isolates are stateless — they may be reused across requests on the same edge location, but have no guaranteed longevity. Use ctx.waitUntil() to flush after the response is sent. The ctx / waitUntil source depends on your framework:
| Framework | waitUntil access |
|---|---|
| React Router 7 | context.cloudflare.ctx.waitUntil() |
| SvelteKit | platform.ctx.waitUntil() |
| Astro 5 | Astro.locals.runtime.ctx.waitUntil() |
| Astro 6+ | Astro.locals.cfContext.waitUntil() |
| Hono | c.executionCtx.waitUntil() |
| Raw Workers | ctx.waitUntil() (the ExecutionContext directly) |
As a framework-agnostic alternative, you can import { waitUntil } from 'cloudflare:workers' and call it from anywhere.
We recommend creating a new PostHog client per request. Workers may reuse globals across requests on the same isolate, but the shutdown/flush lifecycle makes per-request instantiation safer.
Durable Objects and local evaluation
When using local evaluation inside a Durable Object, disable automatic flag polling if the object needs to hibernate or shut down while idle. Polling timers can keep the object active and extend billable duration, even when flag definitions come from a shared cache.
With Cloudflare compatibility dates of 2026-10-01 or later, pending timers also prevent idle eviction without a connected client. Earlier dates can enable this behavior with durable_object_io_tasks_prevent_eviction. See Cloudflare's lifecycle change.
In posthog-node 5.55.0 or later, set featureFlagsPollingInterval to null to disable feature flag polling timers:
Store your feature flags secure API key in the POSTHOG_FEATURE_FLAGS_SECURE_API_KEY secret binding.
Definitions still load when you create the client. The SDK consults your configured cache provider or fetches them from PostHog. If the cache is empty and the client has no definitions, the SDK fetches them from PostHog. Local evaluation waits for the initial load.
After loading, the client keeps its in-memory definitions without refreshing them automatically. If your application needs updated definitions, await posthog.reloadFeatureFlags() explicitly reloads them. Choose a refresh frequency based on how quickly your application needs flag changes. Do not reload on every request or WebSocket message: each reload can fetch definitions from PostHog and increase costs. A failed refresh can leave the previous definitions in use.
You can also use a separate Worker with a Cron Trigger to refresh a shared KV cache. New clients load the updated cache. Existing clients still need an explicit reload to read it.
This option disables only the feature flag polling timer. Requests, event flushing, and other pending work can still delay hibernation or shutdown.