Lambda@Edge was always the most operationally painful part of the AWS stack.
Not because the concept was wrong. Running auth logic at the edge before requests reach the origin is a good idea. The problem was everything around it: the deployment model, the debugging story, the constraints, and the way it forced auth into a completely separate compute lifecycle from the rest of the application.
When I moved the portfolio to Bunny, replacing Lambda@Edge was the piece I was most relieved to get right.
What Lambda@Edge was actually doing
Across the portfolio, Lambda@Edge handled a few things at the CDN layer:
CloudFront Functions handled lighter work: simple redirects, cache key normalization, and header stripping. The two layers worked together, but maintaining them meant understanding which logic belonged in which execution model, because CloudFront Functions and Lambda@Edge had different runtime constraints, different deployment patterns, and different failure modes.
Why Lambda@Edge was painful
The deployment lifecycle was the worst part.
Lambda@Edge functions are replicated across CloudFront’s global edge network. Deploying a new version meant publishing a numbered Lambda version, associating it with the CloudFront distribution, and waiting for the distribution to propagate. That propagation could take fifteen to twenty minutes. If you got the auth logic wrong, you were waiting fifteen minutes to fix it.
There was no practical way to test Lambda@Edge locally against real CloudFront behavior. You could unit test the function in isolation, but the integration with CloudFront’s request and response events, the header constraints, and the body handling all had edge cases that only surfaced in production.
Logging was scattered. Lambda@Edge logs go to CloudWatch in the region closest to where the function executed. If a user in Europe hit an auth error, the logs were in eu-west. If a user in Asia hit the same error, the logs were in ap-northeast. Debugging a single auth issue could mean searching CloudWatch across half a dozen regions.
And the constraints were tight. Lambda@Edge had a 5-second timeout for origin request events, a 30-second timeout for origin response events, a 1MB response body limit, and no environment variable support. Every piece of configuration had to be baked into the function code or fetched at runtime from another service, which added latency to the auth path.
What replaced it
Two things: Bunny’s edge rules and the Magic Container itself.
Bunny’s edge rules handle the simple stuff. Redirects, header manipulation, path normalization, basic access control. These are configured declaratively on the pull zone. No code to deploy. No propagation delay. Changes take effect quickly because they are configuration, not replicated compute.
The heavier auth logic moved into the Magic Container. Token validation, session management, and authorization decisions all happen inside the application now. When a request arrives at the container, the first thing the routing layer does is check auth. If the request is not authenticated, the container handles the redirect or returns a 401 directly.
This means auth is part of the application. Not a separate compute layer. Not a separate deployment lifecycle. Not a separate logging destination.
Auth became testable
This was the change that mattered most for confidence.
On Lambda@Edge, testing auth meant either unit testing the function in isolation without real CloudFront event shapes, or deploying to production and checking manually. The gap between those two was where bugs lived.
With auth inside the container, the entire auth flow is testable locally. Start the container, send a request without a token, verify the redirect. Send a request with a valid token, verify it reaches the handler. Send a request with an expired token, verify the rejection. All of this runs in the same test suite as the rest of the application.
No mocking CloudFront events. No simulating edge execution contexts. No wondering whether the test environment actually matches what happens in production.
The debugging story improved
On AWS, an auth failure could mean checking CloudWatch in the region where the edge function ran, cross-referencing with CloudFront access logs, and then checking the origin Lambda logs to see if the request even made it that far.
On Bunny, an auth failure means checking the container logs. One place. One log stream. The auth decision and the downstream handler execution are in the same process, so the full request lifecycle is visible without correlating across services.
For a portfolio where I am the only operator, this is the difference between diagnosing a problem in two minutes and diagnosing it in twenty.
Edge rules handle the lightweight cases
Not everything needs to run in the container.
Bunny’s edge rules handle the cases that do not require application logic: redirecting HTTP to HTTPS, stripping trailing slashes, adding security headers, blocking certain paths. These are the things that CloudFront Functions used to handle, but without the code deployment and versioning overhead.
The distinction is clean. If the decision can be made from the URL, headers, or request method alone, it belongs in the edge rules. If the decision requires session state, a database lookup, or application context, it belongs in the container.
That split eliminated the ambiguity that existed on AWS, where you had to decide whether something belonged in CloudFront Functions, Lambda@Edge viewer request, Lambda@Edge origin request, or the origin itself. Four possible execution points for what should be a straightforward question about where auth logic runs.
Security posture got simpler
Fewer moving parts means fewer places to misconfigure.
On AWS, the auth surface included Lambda@Edge IAM roles, CloudFront cache behaviors, origin request policies, Lambda function permissions, and the interaction between all of them. A misconfigured cache behavior could serve authenticated content to unauthenticated users. A stale Lambda@Edge version could let expired tokens through.
On Bunny, the auth surface is the container and the edge rules. The container validates tokens. The edge rules handle redirects and headers. There are two things to get right instead of five.
That does not mean the security work is trivial. It means the attack surface is smaller and the configuration is easier to audit. When you can read the entire auth story in one place, you are more likely to notice when something is wrong.
What I gave up
Geographic distribution of auth decisions. Lambda@Edge ran auth checks at the CDN edge closest to the user, which meant the auth latency was minimal regardless of where the user was. With auth in the Magic Container, the auth check happens wherever the container runs.
In practice, this mattered less than expected. Magic Containers run on Bunny’s edge infrastructure, so they are already distributed. The latency difference between running auth in a Lambda@Edge function and running it in a container on the same edge network was negligible for this portfolio’s traffic patterns.
I also gave up the ability to modify CloudFront responses after the origin returned them. Lambda@Edge origin response events let you manipulate headers and bodies on the way back from the origin. On Bunny, the container controls its own responses directly, so this capability was not lost, it just moved.
Why this was the right move
Lambda@Edge solved a real problem. But it solved it by adding a separate compute layer with its own deployment model, its own constraints, and its own operational overhead.
For a portfolio of real products maintained by one person, the better answer was to put auth where it belongs: inside the application. Testable, debuggable, deployable as part of the same unit as everything else.
The edge still does work. It handles the lightweight decisions that do not need application context. But the heavy auth logic is no longer split across a separate compute boundary with a fifteen-minute deploy cycle and region-scattered logs.
That is a better shape. And the portfolio is easier to operate because of it.