Send AWS CloudWatch logs to PostHog
Contents
PostHog has an HTTP endpoint that accepts deliveries from an Amazon Data Firehose stream, so you don't have to run a forwarder. A CloudWatch subscription filter copies a log group into the stream, and the stream posts batches to PostHog. Everything runs in your AWS account.
This works for any CloudWatch log group: Lambda, RDS, ECS, API Gateway, VPC flow logs, and anything your own code writes.
Before you start
You need:
- Your project API key, from Settings > Project > Project API key.
- Permission in AWS to create a Firehose stream, two IAM roles, an S3 bucket, and a subscription filter.
- The names of the log groups you want to send.
Firehose bills for the data it ingests. Check the Firehose pricing page before you subscribe a high-volume log group.
Your endpoint URL
| PostHog Cloud | Endpoint URL |
|---|---|
| US | https://us.i.posthog.com/i/v1/logs/aws/firehose |
| EU | https://eu.i.posthog.com/i/v1/logs/aws/firehose |
If you self-host, use your own ingestion host with the same path. Firehose delivers only to HTTPS endpoints, so the host needs to be reachable from AWS over HTTPS with a valid certificate.
Set it up with CloudFormation
The template below creates the stream, the roles, a backup bucket, and a subscription filter for one log group.
- Save it as
posthog-cloudwatch-logs.yaml. - Open CloudFormation > Create stack > With new resources, choose Upload a template file, and pick the file.
- Fill in
PostHogEndpointUrl,PostHogAccessKey(your project API key), andLogGroupName. - Create the stack.
To add more log groups later, create another subscription filter pointing at the DeliveryStreamArn output, reusing the LogsToFirehoseRoleArn role. You don't need a second stream.
Set it up in the console
To set it up by hand:
- Open Amazon Data Firehose > Create Firehose stream. Set the source to Direct PUT and the destination to HTTP Endpoint.
- Set the endpoint URL from the table above, and paste your project API key into Access key. Use the settings below.
- Open CloudWatch > Log groups, pick a group, and choose Actions > Subscription filters > Create Amazon Data Firehose subscription filter. Select the stream and leave the filter pattern empty to forward every line.
Repeat step 3 for each log group you want to send.
Stream settings
| Setting | Value | Why |
|---|---|---|
| Buffer size | 1 MB | Stays under PostHog's 8 MB limit once Firehose encodes the records. |
| Buffer interval | 60 seconds | Logs arrive within about a minute. |
| Content encoding | GZIP | Fewer bytes on the wire. |
| Retry duration | 300 seconds | Retries for five minutes before writing to the backup bucket. |
| S3 backup | Failed data only | Keeps a copy of what PostHog could not accept. |
What your logs look like
Each CloudWatch log event becomes one log in PostHog. The message is the log body, and the event keeps its original CloudWatch timestamp.
These attributes describe where the log came from. They are the same for every log in a log group:
| Attribute | Value |
|---|---|
cloud.provider | aws |
cloud.account.id | The AWS account that owns the log group |
cloud.region | The region the stream runs in |
aws.log.group.name | The log group |
aws.log.stream.name | The log stream |
These describe the individual event:
| Attribute | Value |
|---|---|
aws.log.event.id | The id CloudWatch gave the event |
aws.subscription_filters | The subscription filters that matched, separated by commas |
Service name comes from the log group. /aws/lambda/checkout-api becomes checkout-api, /aws/rds/instance/orders-db/postgresql becomes orders-db, and /aws/vpc/flowlogs becomes vpc-flow-logs. A log group outside the AWS naming conventions keeps its full name, so my-app-logs stays my-app-logs. To set the name yourself, add a service.name common attribute to the Firehose stream.
Severity comes from a level, severity, log.level or config.log_level key when the message is JSON. In a plain-text line PostHog reads the first three words for a level such as ERROR, WARN or DEBUG, so ERROR payment declined and 2026-09-16T10:00:00Z INFO started both resolve. Common spellings such as warning, err and critical are recognized. Anything else is stored as info, so plain line with error later in it stays info.
Logs appear in the Logs view within a couple of minutes, depending on your buffer settings. Filter by service.name, severity, or any of the attributes above.
Limits and failures
- PostHog rejects a delivery above 8 MB, and Firehose writes it to the backup bucket instead of retrying. A 1 MB buffer stays well under.
- Delivery is at-least-once. If PostHog fails part way through a batch, Firehose resends the whole batch and some lines can appear twice. Copies of the same event share an
aws.log.event.id, so you can tell a duplicate from two events that happen to carry the same message. - A record that PostHog can't decode is skipped, and the rest of the batch is still accepted.
Troubleshooting
No logs arrive. Open the stream's Monitoring tab in the Firehose console. If DeliveryToHttpEndpoint.Success is flat, check the log group's subscription filter exists and points at this stream. If deliveries are failing, the Error logs tab shows PostHog's response.
Every delivery fails with 401. The access key isn't a valid project API key. Copy it again from Settings > Project > Project API key, and check it belongs to the project you're looking at.
Every delivery fails with 413. The buffer size is too large for the endpoint. Set it to 1 MB.
Logs arrive under the wrong service name. Add a service.name common attribute to the Firehose stream, which overrides the name inferred from the log group.











