AWS CDK S3 to Lambda via EventBridge: The Cost Model Nobody Checks First
Plenty of S3-to-Lambda stacks route object events through CloudTrail data events into EventBridge, then pay per event for what is really just plumbing. S3 already sends events to EventBridge natively, for free. Here is the cost model, when CloudTrail is genuinely worth it, and the failure paths nobody diagrams.
A client pinged me a few weeks ago. Their AWS bill had crept up - not "CFO calling you at home" levels, but enough that someone finally opened Cost Explorer and started asking questions. The line that stood out was CloudTrail. Not Lambda, not S3. CloudTrail. Their aws cdk s3 eventbridge lambda trigger pipeline was pushing every S3 object event through CloudTrail data events into EventBridge, and they were paying per event for what is, in most designs, free plumbing.
The architecture worked. It just cost more than it had any reason to.
Who This Is For
If you run, or are about to build, an S3-triggered serverless workflow in CDK TypeScript where a Lambda is VPC-placed, KMS-encrypted, and the events need to fan out to more than one target, this is for you. You should be comfortable reading a CDK stack and you should know roughly how IAM, VPC endpoints, and KMS behave. I am not going to explain what a Lambda function is.
The Decision That Sets the Bill
There are two genuinely different ways to get an S3 object event into EventBridge, and people conflate them constantly.
One: native S3 to EventBridge. Since late 2021, S3 can send event notifications straight to the default EventBridge bus. You flip one setting on the bucket (EventBridgeConfiguration in CDK), and every Object Created, Object Deleted, and similar event lands in EventBridge with full rule matching and fan-out to as many targets as you want. The S3 to EventBridge events themselves are not billed as custom events. For the eventing path, this is effectively free.
Two: CloudTrail data events into EventBridge. You enable S3 data events on a CloudTrail trail, and EventBridge matches on the CloudTrail event stream. This was the only way to get rich S3 eventing into EventBridge before the native integration existed, and a lot of stacks - and a lot of older blog posts - still reach for it by reflex. CloudTrail data events cost $0.10 per 100,000 events (CloudTrail pricing, checked June 2026). That is the bill my client was paying.
Here is the part worth sitting with: CloudTrail data events are an audit decision, not an eventing decision. If you want a durable, queryable record of every object-level API call for compliance or forensics, data events are how you get it, and they are worth the money. If all you want is "run this Lambda when an object lands," routing through CloudTrail means you are paying for an audit trail you may not have asked for, and using it as a trigger.
So the first question is not how to make this cheaper. It is whether you are paying for audit or for eventing. Most teams that route S3 through CloudTrail wanted eventing and bought audit by accident.
What CloudTrail Data Events Actually Cost
When you do route through CloudTrail, the cost scales directly with object operations, and it is easy to undercount because nobody models it before shipping.
At $0.10 per 100,000 events:
- 1 million PUTs/day = 10 units of 100k = $1/day, about $30/month just for the trail.
- 10 million PUTs/day = 100 units = $10/day, about $300/month.
For a bucket taking 10 million object writes a day, that is roughly $300/month in CloudTrail data event charges while the Lambda doing the actual work might run for $5. The eventing layer costs more than the compute. That is the moment to ask whether you needed CloudTrail in this path at all, because native S3 to EventBridge would have moved those same events for nothing.
The Other Two Cost Layers
The trail is the obvious one. Two more hide in services people do not associate with this pipeline.
VPC endpoints. A VPC-placed Lambda that reads from Secrets Manager and SSM Parameter Store privately needs interface endpoints. Each interface endpoint is $0.01 per hour per AZ, about $7.30/month/AZ (PrivateLink pricing, checked June 2026), plus $0.01/GB processed. Deploy to 3 AZs for production and one endpoint is about $22/month. You usually need at least S3 (gateway endpoint, free), Secrets Manager, and SSM, so the two interface endpoints run about $44/month before data charges. Teams undercount this because it shows up on the bill under "VPC," not "Lambda."
KMS API calls. Customer-managed keys charge $0.03 per 10,000 requests after the first 20,000/month free, plus $1/month per key (KMS pricing, checked June 2026). S3 decrypts on GET and encrypts on PUT, Lambda decrypts environment variables on cold start, CloudWatch Logs encrypts each batch. At low volume this is a couple of dollars and you ignore it. A Lambda invoked 5 million times a month making three KMS calls each is 15 million calls, about $45/month, and that is the kind of number that surprises you because nothing in the design points at it.
CloudWatch Logs retention. The classic silent cost. A log group with no expiry keeps paying storage forever. This stack sets retention explicitly, which is correct. The trap is that if anyone recreates the Lambda by hand outside CDK, the new log group defaults to never-expire and the cost control quietly breaks.
What Can Safely Change
Not all of this is load-bearing. Some of it trims without touching the core design.
Drop CloudTrail from the eventing path. If you are using data events only to trigger Lambda, switch to native S3 to EventBridge and the data event bill goes to zero. Keep a CloudTrail trail for audit if you need one, but stop using it as your trigger. This is the single biggest line item on most of these stacks, and for pure eventing it is avoidable.
If you keep CloudTrail for audit, scope the selectors. You rarely need every object operation on every prefix. Scope the data event selector to the exact prefix and operations the workload cares about (s3://your-bucket/incoming/, PutObject only). On a mixed-workload bucket this cuts data event volume 60 to 80 percent.
Right-size VPC endpoints by AZ. If the workload is not customer-facing, 2 AZs instead of 3 saves about $7.30/month per endpoint per dropped AZ. For dev and staging, a single NAT Gateway can be cheaper than a full set of interface endpoints at low traffic - run the numbers for your call volume.
Set log retention aggressively. 7 days covers most operational debugging, 90 days if security review needs it. Never-expire is almost never the right answer.
Alternatives, Honestly Compared
Native S3 to EventBridge. The default for almost everyone now. Free eventing, rule-based routing, fan-out to many targets, no bucket-notification single-target limit. This is what the CloudTrail path was working around before it existed. If you are starting fresh, start here.
Direct S3 event notification to Lambda (or SNS/SQS). Lowest latency, sub-second, zero eventing cost. The limit is one target per event type per bucket, so fan-out needs SNS in the middle, and you get no audit record. Fine for a single-consumer pipeline, awkward past that.
S3 to SQS to Lambda. Underrated for high-throughput work that does not need EventBridge filtering. SQS gives you batching, a dead-letter queue, and visibility-timeout semantics, and the cost is predictable. I have reached for this on cost-sensitive pipelines specifically because the DLQ behaviour saved me from reprocessing incidents that EventBridge would have dropped on the floor. It looks old next to EventBridge. It is often the correct call.
S3 Batch Operations. Different use case, worth naming: if you are processing objects that already exist rather than reacting to new uploads, Batch Operations is far cheaper than a continuous event pipeline. $0.25 per job plus $1 per million object operations.
CloudTrail data events into EventBridge. Now a narrow choice, not a default. It earns its cost only when you genuinely need object-level audit records and want to route on them: management-event context, cross-account trails, or a compliance requirement that the eventing and the audit be the same record. For plain "run this when an object lands," it is the expensive answer to a question native EventBridge already solved.
Cost Scenarios
Rough numbers for a medium-traffic production workload that has chosen to keep CloudTrail in the path. Your mileage varies.
| Scenario | Monthly CloudTrail | Monthly VPC Endpoints | Monthly KMS | Total |
|---|---|---|---|---|
| 100k PUTs/day, 2 AZ, scoped selectors | ~$3 | ~$44 | ~$2 | ~$49 |
| 1M PUTs/day, 3 AZ, full data events | ~$30 | ~$66 | ~$15 | ~$111 |
| 10M PUTs/day, 3 AZ, full data events | ~$300 | ~$66 | ~$45 | ~$411 |
Lambda execution at 1M invocations/month (512MB, 500ms average) is about $5. At 10M it is about $50. So at the top row, your event pipeline costs eight times your compute. Switching that same workload to native S3 to EventBridge removes the entire CloudTrail column: the 10M/day case drops from about $411 to about $111, and the saving is the CloudTrail line, roughly $300/month, with no architectural loss as long as you did not need the audit trail.
Operational Trade-Offs
A few things that do not show up in a cost calculator.
Latency. CloudTrail data events through EventBridge add a few seconds end to end versus direct notification, because the event has to land in the trail first. For a compliance pipeline processing audit records, fine. For a user-facing feature showing a "processing" spinner, not fine. Native S3 to EventBridge is faster than the CloudTrail path here too, which is one more reason it wins for eventing.
The trail going dark. If you do route through CloudTrail and the trail gets disabled, or the destination bucket policy changes, EventBridge stops receiving S3 events. Silently. There is no DLQ on that path and no native alert on event-delivery failure. You find out when someone notices the Lambda has not run in hours. Put a CloudWatch alarm on EventBridge FailedInvocations and separately monitor that the trail is active.
KMS key deletion. The failure nobody plans for. Delete the CMK or let its key policy drift, and S3 cannot decrypt objects, Lambda cannot read environment variables, and CloudWatch Logs cannot write, all at once, with no graceful degradation. Set the key deletion window to the maximum 30 days, tag the key, and test the key policy in staging before it surprises you in production.
The Over-Correction
I have watched teams swing too hard the other way after seeing a CloudTrail bill.
Ripping CloudTrail out to save $30/month is correct only if you truly do not need the audit trail. The moment a real requirement lands - a GDPR deletion you have to prove, a SOC 2 control, a breach inquiry - you will want that object-level record, and recreating history after the fact is impossible. Move CloudTrail out of the trigger path, by all means. Decide separately, and on the record, whether you still want it as an audit trail.
Same caution on VPC placement and KMS. Pulling the Lambda out of the VPC to dodge endpoint costs means Secrets Manager and SSM traffic leaves the private network. Collapsing to a single shared CMK to cut complexity is fine until the day you have to audit every policy touching that key at once. Cheaper is not free. It just moves the cost somewhere less visible.
Where to Start
Open Cost Explorer and look at whether CloudTrail is a line item on this workload. If it is, the fastest honest win is to ask whether the trail is feeding your trigger or your audit. If it is the trigger, move to native S3 to EventBridge and watch the data event charge disappear. If it is audit, scope the selectors to the prefix and operations you actually need. Then check your VPC endpoint count against the AZs you genuinely require. Those two checks cost nothing and surface most of the savings on this pattern, and neither one weakens the architecture.
Next step
Have the same problem on your stack?
Send the architecture, AWS bill concern, deploy pain, or GenAI reliability issue. I'll find the first real bottleneck and propose a small, reversible fix.
Related reading

Rahul Ladumor
Principal Cloud & AI Platform Architect. AWS Professional certified, 4x AWS Community Builder. I work with teams that have real users, real AWS bills, and real production pressure.
About Rahul →