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:

valid token

no token

simple redirects

header stripping

cache keys

Client Request

CloudFront

Viewer Request

Lambda@Edge

Origin Request

Lambda@Edge

302 Redirect to Login

Origin / API Gateway

Origin Response

Lambda@Edge

Viewer Response

CloudFront Function

CloudFront Function

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.

Edge LocationsCloudFrontLambda (us-east-1)DeveloperEdge LocationsCloudFrontLambda (us-east-1)Developer15-20 minute propagationPublish new versionVersion 42Update distribution behaviorReplicate to all edgesEventually consistentIf wrong, wait 15 min again

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.

HTTPS redirect

header injection

path rewrite

passes

valid

invalid

Client Request

Bunny CDN

Edge Rules

Magic Container

Auth middleware

Route Handler

401 / 302 Redirect

Response

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.

After: Container testing

Start container

Send request: no token

Assert redirect

Send request: valid token

Assert success

Send request: expired token

Assert rejection

Before: Lambda@Edge testing

gap

bug

Unit test in isolation

Deploy to production

Check manually

Wait 15 min to fix

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.

Bunny: debug auth failure

Auth failure

Container logs

Full request lifecycle visible

AWS: debug auth failure

Auth failure

Which edge region?

CloudWatch eu-west?

CloudWatch us-east?

CloudWatch ap-northeast?

Correlate with CF access logs

Check origin Lambda logs

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.

No: URL, headers,

method only

Yes: session state,

DB lookup, user context

Incoming Request

Needs app context?

Bunny Edge Rules

Magic Container

HTTPS redirect

Security headers

Path blocking

Slash normalization

Token validation

Session management

Role-based access

Auth redirects

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.