Sending HTTP log events
Contents
$http_log is PostHog's event for server-side HTTP request logs. Most bots, crawlers, and AI agents never run JavaScript, so the PostHog JavaScript SDK never sees them – the only record of their visit is your server or CDN access log. Forward those log entries as $http_log events and bot and traffic detection classifies them alongside your $pageview events, powering bot analytics in web analytics.
You can send $http_log events from anywhere that can make an HTTP request: an edge worker, a reverse proxy, your application server, or a log shipper. This page documents the payload so you (or your coding agent) can implement it on any platform.
The payload
Send events to the capture API like any other event. Here's the shape:
POST it to <ph_client_api_host>/i/v0/e/, or wrap multiple events in the batch API (<ph_client_api_host>/batch/) if you ship logs in batches.
Properties
| Property | Required | Purpose |
|---|---|---|
$raw_user_agent | Yes | The client's User-Agent header. Bot detection reads this – without it, every event classifies as Automation. |
$current_url | Yes | The full request URL. Web analytics breakdowns and UTM attribution read it. |
$host | Recommended | The request host. Derive it from the URL if your log line doesn't carry it separately. |
$pathname | Recommended | The request path without the query string. |
$process_person_profile | Recommended | Set to false so log traffic doesn't create a person profile per distinct ID. See below. |
$ip | Recommended | The client IP. Enables GeoIP enrichment and IP-based bot classification. |
$referrer | Optional | The Referer header. |
method | Optional | The HTTP method. |
status_code | Optional | The response status code. |
timestamp (top level) | Optional | ISO 8601 time of the request. Defaults to ingestion time, so set it if you ship logs with a delay. |
Anything else from your log line (data center region, cache status, TLS fingerprint, bot scores from your CDN) can go in properties under any name and stays queryable.
Distinct IDs and person profiles
Server requests carry no PostHog cookie, so you decide the distinct_id yourself:
- Use a derived, per-client ID: a hash of IP, host, and user agent gives one stable identity per client, which keeps unique-visitor counts meaningful. Prefix it with
http_log_so you can recognize log traffic in queries. - Avoid a single shared ID (all traffic counts as one visitor) and avoid random per-request IDs (every request counts as a new visitor).
- Send events as anonymous (
$process_person_profile: false). High-cardinality log traffic would otherwise create a person profile per distinct ID, which adds cost and slows person-joined queries. Bot detection reads event properties, not the profile, so nothing is lost.
Controlling volume
A single page view fans out into many sub-resource requests (JS, CSS, images, fonts). If you only care about page and API traffic, skip requests whose path ends in an asset extension before sending. Keeping paths with no extension (plus .html) gets you close to a document-level stream at a fraction of the volume.
Cloudflare Worker example
This pass-through Worker reports each request in the background, so it never delays a response. It works on every Cloudflare plan. Store your project token as a Worker variable, and add a route like example.com/* so it runs on your traffic.
Only traffic proxied through Cloudflare (orange-cloud DNS) reaches Workers. cloudflare_bot_score only populates if your zone has Cloudflare Bot Management; without it, the optional chaining leaves the property out. On an Enterprise plan, consider Logpush instead – it keeps log delivery off the request path entirely.
Cloudflare Logpush example (Enterprise plan)
Cloudflare Logpush pushes the HTTP requests dataset to an HTTP endpoint as gzipped, newline-delimited JSON batches. PostHog's capture API expects individual events, so the pattern is a small relay Worker: Logpush delivers batches to the Worker, and the Worker maps each record to a $http_log event and forwards them to the batch API. Unlike the pass-through Worker above, nothing runs on your visitors' request path.
Deploy this as a Worker and note its URL (a workers.dev URL works).
Then create the Logpush job on your zone, under Analytics & Logs > Logpush > Create a Logpush job:
- Pick the HTTP requests dataset and the HTTP destination.
- Set the destination to your Worker URL, passing the shared secret as a header parameter:
https://<your-worker>.workers.dev?header_Authorization=Bearer%20<secret>. Store the same secret as the Worker'sLOGPUSH_SECRETvariable. - Select at least these fields:
EdgeStartTimestamp,ClientIP,ClientRequestHost,ClientRequestMethod,ClientRequestURI,ClientRequestUserAgent,ClientRequestReferer,EdgeResponseStatus,ClientCountry,RayID. WithoutClientRequestUserAgent, bot detection classifies everything asAutomation. - In the advanced options, set the timestamp format to RFC3339 so
EdgeStartTimestampmaps directly onto the event timestamp. - Cloudflare validates the destination by sending a gzipped test file when you create the job. The Worker acknowledges it automatically (records without
ClientRequestHostare skipped).
Logpush also supports a sampling rate in the job settings if you want to cap volume at the source.
Server middleware example
The same idea from your own server, shown as Express middleware. Fire the capture request after the response finishes so logging never blocks the request path. Behind a load balancer or proxy, configure trust proxy first, so req.ip, req.hostname, and req.protocol reflect the client rather than the proxy.
Managed alternatives
If you'd rather not write the capture call yourself:
- Vercel logs source – in PostHog, go to Data pipeline > Sources and add the Vercel logs source, then point a Vercel log drain at the generated endpoint. Distinct ID hashing, person processing, and page-route filtering are settings on the source.
- Check Data pipeline > Sources in your project for other managed sources as we add them.
Once events are flowing, head to bot and traffic detection for the classification functions and virtual properties you can query them with.