Cloudflare reverse proxy

Before you start
  • If you use a self-hosted proxy, PostHog can't help troubleshoot. Use our managed reverse proxy if you want support.
  • Use domains matching your PostHog region: us.i.posthog.com for US, eu.i.posthog.com for EU.
  • Don't use obvious path names like /analytics, /tracking, /telemetry, or /posthog. Blockers will catch them. Use something unique to your app instead.

This guide shows you how to use Cloudflare as a reverse proxy for PostHog.

Prerequisites

  • A Cloudflare account
  • A domain managed by Cloudflare
  • For option 2: Cloudflare Enterprise plan

Choose your setup option

Cloudflare sits between your users and PostHog. When a user triggers an event, the request goes to Cloudflare first, then Cloudflare forwards it to PostHog. This hides PostHog's domains from ad blockers.

Cloudflare offers two approaches for proxying. Choose based on your Cloudflare plan and technical preferences:

  • Option 1: Cloudflare Workers: Runs serverless JavaScript on Cloudflare's edge network to intercept and forward requests. You write code that handles routing logic, header manipulation, and caching. This gives you full control but requires maintaining code. Works on all Cloudflare plans including free.
  • Option 2: DNS and Page Rules: Uses Cloudflare's DNS to route traffic and Page Rules to rewrite headers. Simpler configuration with no code to maintain, but requires an Enterprise plan and has less flexibility.

Option 1: Cloudflare Workers

This method uses Cloudflare's serverless platform to run code that proxies requests to PostHog.

  1. Create a Cloudflare Worker

    Open your Cloudflare dashboard and follow Cloudflare's Workers guide to create a new worker.

    Cloudflare Workers are serverless functions that run on Cloudflare's edge network. Your worker will intercept requests to your subdomain and forward them to PostHog with the correct headers.

  2. Add the proxy code

    Replace the default worker code with this:

    const API_HOST = "us.i.posthog.com"
    const ASSET_HOST = "us-assets.i.posthog.com"
    async function handleRequest(request, ctx) {
    const url = new URL(request.url)
    const pathname = url.pathname
    const search = url.search
    const pathWithParams = pathname + search
    if (pathname.startsWith("/static/")) {
    return retrieveStatic(request, pathWithParams, ctx)
    } else {
    return forwardRequest(request, pathWithParams)
    }
    }
    async function retrieveStatic(request, pathname, ctx) {
    let response = await caches.default.match(request)
    if (!response) {
    response = await fetch(`https://${ASSET_HOST}${pathname}`)
    ctx.waitUntil(caches.default.put(request, response.clone()))
    }
    return response
    }
    async function forwardRequest(request, pathWithSearch) {
    const ip = request.headers.get("CF-Connecting-IP") || ""
    const originHeaders = new Headers(request.headers)
    originHeaders.delete("cookie")
    originHeaders.set("X-Forwarded-For", ip)
    const originRequest = new Request(`https://${API_HOST}${pathWithSearch}`, {
    method: request.method,
    headers: originHeaders,
    body: request.body,
    redirect: request.redirect
    })
    return await fetch(originRequest)
    }
    export default {
    async fetch(request, env, ctx) {
    return handleRequest(request, ctx);
    }
    };

    This code does three things:

    • Routes requests: The handleRequest function checks if the request is for static assets (/static/*) or API calls. Static assets go to PostHog's asset server, everything else goes to the main API.
    • Caches static assets: The retrieveStatic function caches PostHog's JavaScript SDK and other static files in Cloudflare's cache. This improves performance and reduces load on PostHog's servers.
    • Preserves user location: The forwardRequest function captures the real user IP from Cloudflare's CF-Connecting-IP header and sets it as X-Forwarded-For. This ensures PostHog records accurate user locations instead of showing all users at Cloudflare's data center locations. It also removes cookies for privacy.
  3. Add a custom domain to your worker

    In the Cloudflare dashboard, follow Cloudflare's custom domains guide to add a subdomain like e.yourdomain.com to your worker.

    Using your own domain instead of the default *.workers.dev domain makes the proxy less likely to be blocked. Ad blockers recognize and block *.workers.dev patterns.

    Avoid obvious terms like tracking, analytics, posthog, or telemetry in your subdomain name. Use something neutral like e, ph, or ingest instead.

  4. Update your PostHog SDK

    In your application code, update your PostHog initialization to use your worker's domain:

    posthog.init('<ph_project_api_key>', {
    api_host: 'https://e.yourdomain.com',
    ui_host: 'https://us.posthog.com'
    })

    Replace e.yourdomain.com with your actual subdomain.

    The ui_host must point to PostHog's actual domain so features like the toolbar link correctly.

  5. Verify your setup

    Checkpoint

    Confirm events are flowing through your worker:

    1. Open your browser's developer tools and go to the Network tab
    2. Trigger an event in your app, like a page view
    3. Look for a request to your worker subdomain (e.g., e.yourdomain.com)
    4. Verify the response is 200 OK
    5. Check the PostHog app to confirm events appear

    If you see errors, check troubleshooting below.

Option 2: DNS and Page Rules

This method uses Cloudflare's DNS and Page Rules to route traffic without writing code. It requires a Cloudflare Enterprise plan.

  1. Create a DNS CNAME record

    Open your Cloudflare dashboard and follow Cloudflare's DNS records guide to create a CNAME record.

    Configure the record:

    • Name: Your subdomain, like e
    • Target: us-proxy-direct.i.posthog.com or eu-proxy-direct.i.posthog.com for EU region
    • Proxy status: Proxied (configure by clicking the orange cloud icon)

    The CNAME points your subdomain to PostHog's proxy endpoint. The orange cloud means Cloudflare will proxy the traffic instead of just doing DNS resolution.

    Avoid obvious terms like tracking, analytics, posthog, or telemetry in your subdomain. Use something neutral like e, ph, or ingest instead.

  2. Create a Page Rule to rewrite the Host header

    In the Cloudflare dashboard, follow Cloudflare's Page Rules guide to create a new rule.

    Configure the rule:

    • URL pattern: e.yourdomain.com/* (replace with your actual subdomain)
    • Setting: Host Header Override
    • Value: us-proxy-direct.i.posthog.com or eu-proxy-direct.i.posthog.com

    The Host Header Override tells PostHog which domain the request is for. Without this, PostHog won't know how to route your request and you'll get 401 errors.

  3. Update your PostHog SDK

    In your application code, update your PostHog initialization to use your CNAME domain:

    posthog.init('<ph_project_api_key>', {
    api_host: 'https://e.yourdomain.com',
    ui_host: 'https://us.posthog.com'
    })

    Replace e.yourdomain.com with your actual subdomain.

    The ui_host must point to PostHog's actual domain so features like the toolbar link correctly.

  4. Verify your setup

    Checkpoint

    Confirm events are flowing through your DNS proxy:

    1. Open your browser's developer tools and go to the Network tab
    2. Trigger an event in your app, like a page view
    3. Look for a request to your subdomain (e.g., e.yourdomain.com)
    4. Verify the response is 200 OK
    5. Check the PostHog app to confirm events appear

    If you see errors, check troubleshooting below.

Troubleshooting

Worker size limits exceeded

Cloudflare Workers have request and response size limits:

  • Free plan: 10MB request, 50MB response
  • Paid plans: Higher limits available

PostHog events can be up to 1MB and session recordings up to 64MB per message. On the free plan, very large recordings might hit the response limit.

If you see errors about size limits, upgrade to a paid Cloudflare plan or contact Cloudflare support about increasing limits.

User locations show as Cloudflare data center locations

If all your users appear to be in the same location, your Worker code is missing the IP forwarding logic.

The worker code in step 2 includes X-Forwarded-For header handling to preserve real user IPs. If you're using older worker code, update it to include this line in the forwardRequest function:

JavaScript
originHeaders.set("X-Forwarded-For", request.headers.get("CF-Connecting-IP") || "")

CORS errors in browser console

If you see No 'Access-Control-Allow-Origin' header errors:

For Workers: Add CORS headers to your worker code. Insert this before the export default line:

JavaScript
function addCorsHeaders(response) {
const newHeaders = new Headers(response.headers)
newHeaders.set("Access-Control-Allow-Origin", "*")
newHeaders.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
newHeaders.set("Access-Control-Allow-Headers", "*")
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
})
}

Then modify your handleRequest to use it:

JavaScript
const response = pathname.startsWith("/static/")
? await retrieveStatic(request, pathWithParams, ctx)
: await forwardRequest(request, pathWithParams)
return addCorsHeaders(response)

For Page Rules: Add these settings to your Page Rule:

  • Disable Security: On
  • SSL: Full
  • Disable Web Application Firewall: On

Warning: Be aware that disabling security features creates vulnerabilities. Only do this if CORS errors persist after trying other solutions.

301 redirects causing failures

If you see 301 Moved Permanently responses that cause CORS errors, your Cloudflare SSL mode is likely set to flexible.

PostHog requires HTTPS. When SSL is flexible, Cloudflare makes HTTP requests to the origin, which redirects to HTTPS. This redirect breaks CORS.

To fix this:

  1. In the Cloudflare dashboard, go to SSL/TLS
  2. Change the SSL mode to Full or Full (strict)

Unexpected token 'export' error in Worker

If your worker fails with Unexpected token 'export', you're using the wrong module format.

When creating a worker, Cloudflare offers two formats:

  • Service Worker (older format)
  • ES Modules (newer format, required for the code above)

The worker code in this guide uses ES Modules syntax. When creating your worker, make sure you select the ES Modules format, not Service Worker.

Page Rules not taking effect

If events aren't reaching PostHog with DNS and Page Rules:

  1. Verify your Page Rule URL pattern matches your subdomain exactly
  2. Check that the Host Header Override value matches PostHog's proxy domain, us-proxy-direct.i.posthog.com or eu-proxy-direct.i.posthog.com
  3. Confirm your DNS record's proxy status is set to proxied (orange cloud), not DNS only (gray cloud)
  4. Check that your CNAME target matches your PostHog region

Page Rules apply in order. If you have multiple rules, make sure the PostHog rule has priority.

Community questions

Was this page useful?

Questions about this page? or post a community question.