AWS Lambda@Edge: Customize CloudFront Responses at the Edge (2026)

Lambda@Edge lets you run Node.js functions in AWS edge locations worldwide — intercepting and modifying CloudFront requests and responses before they ever reach your origin server. This guide covers everything from the four event triggers to real-world use cases including A/B testing, security headers, authentication, and on-the-fly image resizing.

1. What is Lambda@Edge?

Lambda@Edge is an extension of AWS Lambda that runs your code at Amazon CloudFront's global edge locations — currently over 600 points of presence across 90+ cities in 47 countries. Instead of routing every request back to a single origin region such as us-east-1, Lambda@Edge intercepts traffic at whichever edge node is geographically closest to the end user.

Traditional Lambda functions are deployed in a specific AWS region. When a user in Tokyo hits your API Gateway endpoint hosted in Virginia, the request travels roughly 14,000 km round-trip. With Lambda@Edge, your function executes inside the Tokyo edge location — reducing latency by an order of magnitude for latency-sensitive operations like authentication, redirects, and personalisation.

Lambda@Edge was launched in 2017 and has matured significantly since. In 2026 it supports Node.js 18.x and 20.x runtimes, with up to 10 GB memory for origin triggers and 128 MB for viewer triggers. Python support is not available for Lambda@Edge (only CloudFront Functions support a subset of JavaScript); if you need Python at the edge you must use AWS Graviton-backed Lambda URLs behind CloudFront instead.

The key mental model is the request/response pipeline. Every HTTP transaction through CloudFront passes through up to four checkpoints, and Lambda@Edge can hook into any or all of them. This pipeline approach lets you layer multiple transformations — for example, first authenticate the user, then rewrite the URL, then add security headers — without touching your origin application at all.

Key insight: Lambda@Edge functions must always be deployed in us-east-1 (N. Virginia), regardless of where your CloudFront distribution's origin lives. AWS replicates the function automatically to every edge location that serves your distribution.

Common production use cases include: personalising content by geo-location or device type, enforcing authentication before requests reach costly origin compute, rewriting legacy URLs without changing application code, stripping or adding HTTP headers, and implementing dark-launch traffic splitting for gradual feature rollouts.

2. Lambda@Edge vs CloudFront Functions

AWS offers two mechanisms for running code at the CloudFront edge: Lambda@Edge and the newer, lighter-weight CloudFront Functions. Choosing between them is one of the first decisions you will make when architecting an edge solution. They share the same conceptual model but differ significantly in runtime, limits, and pricing.

CloudFront Functions (launched 2021) execute in a nano-JavaScript runtime that is purpose-built for sub-millisecond viewer-facing operations. They can only be attached to the viewer-request and viewer-response events, they have no network access, and they are limited to 10 KB of code and 2 MB of memory. What they lack in power they make up for in speed and price — they execute at over 10 million requests per second globally and cost roughly 1/6 of Lambda@Edge for equivalent viewer-facing workloads.

Lambda@Edge can attach to all four events, supports up to 50 MB deployment packages (unzipped), can make network calls to external services, and has a maximum execution time of 30 seconds on origin events. It is the right choice for anything that requires I/O, complex logic, or third-party SDK calls.

Feature CloudFront Functions Lambda@Edge
Supported eventsviewer-request, viewer-responseAll four events
Max execution time1 ms5 s (viewer), 30 s (origin)
Max memory2 MB128 MB (viewer), 10 GB (origin)
Network accessNoYes
File system accessNo/tmp (512 MB)
Package size10 KB50 MB (zipped)
RuntimeCloudFront JS (ES5.1+)Node.js 18.x / 20.x
Price per 1M requests$0.10$0.60 (viewer), $0.60 (origin)
Compute priceIncluded in request price$0.00000625001 per 128 MB-s
Best forHeader manipulation, simple redirects, cache key normalisationAuth, A/B testing, image processing, API calls

A practical rule of thumb: if your logic can complete in under 1 ms without any network I/O and fits in 10 KB, use CloudFront Functions. For everything else, use Lambda@Edge. Many production architectures combine both — a CloudFront Function normalises the cache key (stripping irrelevant query strings) while a Lambda@Edge function on the origin-request event performs authentication and content personalisation.

3. The Four CloudFront Event Triggers

Every HTTP request handled by CloudFront passes through a well-defined pipeline. Lambda@Edge can intercept four distinct checkpoints in this pipeline. Understanding the order, the data available at each point, and the latency implications is essential for designing effective edge logic.

viewer-request fires after CloudFront receives a request from the viewer (browser or API client) but before the cache is consulted. This is the earliest possible intervention point. Functions here can modify, reject, or short-circuit the request entirely — returning a response without ever consulting the cache or origin. Use this for authentication, bot detection, and URL normalisation. Runs on every request (cache miss and cache hit alike). Memory limit: 128 MB. Timeout: 5 seconds.

origin-request fires only on cache misses, after CloudFront decides it needs to fetch from the origin. The function sees the final request that CloudFront would send to the origin and can modify it — changing the origin path, injecting headers, or even switching to a completely different origin. This is the right place for A/B testing (route 10% of traffic to origin-B), URL rewriting, and dynamic origin selection. Memory limit: 10 GB. Timeout: 30 seconds.

origin-response fires after CloudFront receives the response from the origin but before it is cached. Functions here can modify response headers or body before caching — ideal for stripping internal debug headers, normalising cache-control directives, or triggering image transformations that should be cached. Memory limit: 10 GB. Timeout: 30 seconds.

viewer-response fires before CloudFront sends the response to the viewer, whether the response came from the cache or from the origin. This is the last opportunity to add or modify headers. Security headers (HSTS, CSP, X-Frame-Options) are a classic use case because they must appear on every response regardless of whether it was cached. Memory limit: 128 MB. Timeout: 5 seconds.

Pipeline order: Viewer Request → (cache check) → Origin Request → Origin → Origin Response → (cache store) → Viewer Response → Client

One important constraint: Lambda@Edge functions cannot write to the response body on viewer-request or origin-request events unless they are generating a completely new response (i.e., not forwarding to the origin at all). They can freely read and modify headers and query strings at every event type.

4. Setting Up Lambda@Edge

Deploying Lambda@Edge requires several configuration steps that differ from standard Lambda. The function must live in us-east-1, use a specific IAM execution role that trusts both lambda.amazonaws.com and edgelambda.amazonaws.com, and be published as a numbered version (not $LATEST) before associating it with a CloudFront behaviour.

Step 1 — Create the IAM role. The trust policy must include both Lambda service principals:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Service": [
        "lambda.amazonaws.com",
        "edgelambda.amazonaws.com"
      ]
    },
    "Action": "sts:AssumeRole"
  }]
}

Attach the AWSLambdaBasicExecutionRole managed policy to allow the function to write logs. If the function needs to call other AWS services (e.g., reading secrets from Parameter Store, calling DynamoDB), attach the appropriate additional policies. Remember that execution logs will be written to CloudWatch in whichever region the edge location is in — not us-east-1 — so the role must have permissions to create log groups in all regions.

Step 2 — Write and deploy the function in us-east-1. Create a Lambda function via the console or CLI, making sure the region is explicitly set:

aws lambda create-function \
  --region us-east-1 \
  --function-name my-edge-function \
  --runtime nodejs20.x \
  --role arn:aws:iam::123456789012:role/lambda-edge-role \
  --handler index.handler \
  --zip-file fileb://function.zip

Step 3 — Publish a version. CloudFront requires a specific version ARN, not $LATEST:

aws lambda publish-version \
  --region us-east-1 \
  --function-name my-edge-function

Step 4 — Associate with a CloudFront behaviour. In the CloudFront console, edit the cache behaviour for the path pattern you want to intercept (e.g., /*), scroll to "Function associations", and select the Lambda@Edge function ARN including the version number (e.g., arn:aws:lambda:us-east-1:123456789012:function:my-edge-function:3). CloudFront will replicate the function globally over the next few minutes.

Deployment time: CloudFront distributions can take 5–15 minutes to fully propagate a Lambda@Edge association change. Plan deployments accordingly and always test in a staging distribution first.

5. Use Case 1: A/B Testing with viewer-request

A/B testing — routing a percentage of traffic to a variant of your application — is one of the most powerful Lambda@Edge use cases. By implementing the split at the edge, you eliminate the need for client-side JavaScript experiments that cause layout flicker, and you avoid the complexity of maintaining A/B routing logic inside your application. The function reads (or sets) a cookie to ensure each user consistently sees the same variant across page loads.

The following viewer-request handler assigns users to either the control group (90%) or the experiment group (10%), persists the assignment via a cookie, and rewrites the request URI to route to the appropriate origin path:

'use strict';

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // Check if user already has an experiment assignment cookie
  let group = null;
  const cookieHeader = headers['cookie'];
  if (cookieHeader) {
    for (const cookie of cookieHeader) {
      const match = cookie.value.match(/experiment=([^;]+)/);
      if (match) {
        group = match[1];
        break;
      }
    }
  }

  // Assign a group if none exists (10% to 'variant', rest to 'control')
  if (!group) {
    group = Math.random() < 0.1 ? 'variant' : 'control';
  }

  // Rewrite URI for variant group to /variant path prefix
  if (group === 'variant' && !request.uri.startsWith('/variant')) {
    request.uri = '/variant' + request.uri;
  }

  // Set the assignment cookie so subsequent requests are consistent
  const setCookie = `experiment=${group}; Path=/; Max-Age=86400; SameSite=Lax`;
  headers['x-experiment-group'] = [{ key: 'X-Experiment-Group', value: group }];

  // Forward a custom header so the origin knows the experiment state
  request.headers['x-experiment-group'] = [{
    key: 'X-Experiment-Group',
    value: group
  }];

  return request;
};

Note that setting a Set-Cookie response header requires intercepting the viewer-response event, because request handlers can only modify the request going forward. A common pattern is to pair this viewer-request function (which reads the cookie and rewrites the URI) with a viewer-response function (which sets the cookie on the way back). Alternatively, include the cookie-setting logic inside the origin application and let the response flow through naturally.

Tip: To analyse experiment results, forward the X-Experiment-Group header to your CloudWatch metrics by configuring CloudFront access logs to include custom headers, then use CloudWatch Insights to compare conversion rates.

6. Use Case 2: URL Rewriting and Redirects

URL rewriting is an ideal fit for the origin-request event. At this point the cache has already been checked and a miss has occurred, so the function can modify the request path before it reaches your origin. This is particularly valuable when you are migrating from an old URL structure to a new one, serving a single-page application where the origin only knows about the root path, or routing multiple logical services from a single CloudFront distribution based on path prefix.

The example below handles three common scenarios: redirecting legacy /blog/post-id URLs to the new /articles/post-id structure, mapping /app/* requests to the SPA's index.html, and stripping a version prefix used for cache busting:

'use strict';

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  const uri = request.uri;

  // 1. Permanent redirect: /blog/* → /articles/*
  if (uri.startsWith('/blog/')) {
    return {
      status: '301',
      statusDescription: 'Moved Permanently',
      headers: {
        location: [{
          key: 'Location',
          value: uri.replace('/blog/', '/articles/')
        }],
        'cache-control': [{
          key: 'Cache-Control',
          value: 'max-age=3600'
        }]
      }
    };
  }

  // 2. SPA fallback: /app/* with no file extension → /app/index.html
  if (uri.startsWith('/app/') && !uri.includes('.')) {
    request.uri = '/app/index.html';
    return request;
  }

  // 3. Strip cache-busting version prefix: /v1.2.3/static/... → /static/...
  const versionPrefix = /^\/v\d+\.\d+\.\d+\//;
  if (versionPrefix.test(uri)) {
    request.uri = uri.replace(versionPrefix, '/');
    return request;
  }

  return request;
};

When returning a redirect response (status 301/302) from an origin-request Lambda, the response bypasses the origin entirely — CloudFront immediately sends the redirect to the viewer. This is more efficient than having the origin generate the redirect itself, because the redirect response can also be cached at the edge (configure an appropriate Cache-Control header as shown above).

For dynamic origin selection — routing /api/* requests to an API Gateway endpoint and everything else to an S3 bucket — modify request.origin instead of request.uri. Lambda@Edge allows full control over the origin object, including the domain name, path, custom headers, and SSL settings.

7. Use Case 3: Adding Security Headers on viewer-response

Security headers such as Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options harden your application against a range of attacks including protocol downgrade attacks, clickjacking, MIME sniffing, and cross-site scripting. Applying them inside the application means they only appear on dynamically generated responses — cached CloudFront responses may not include them. Applying them at the viewer-response edge event ensures every single response, whether cached or fresh, carries the full security header set.

'use strict';

exports.handler = async (event) => {
  const response = event.Records[0].cf.response;
  const headers = response.headers;

  // HTTP Strict Transport Security — force HTTPS for 2 years
  headers['strict-transport-security'] = [{
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  }];

  // Prevent clickjacking
  headers['x-frame-options'] = [{
    key: 'X-Frame-Options',
    value: 'DENY'
  }];

  // Prevent MIME sniffing
  headers['x-content-type-options'] = [{
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  }];

  // XSS protection (legacy browsers)
  headers['x-xss-protection'] = [{
    key: 'X-XSS-Protection',
    value: '1; mode=block'
  }];

  // Referrer policy
  headers['referrer-policy'] = [{
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin'
  }];

  // Content Security Policy — adjust sources for your app
  headers['content-security-policy'] = [{
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com",
      "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
      "font-src 'self' https://fonts.gstatic.com",
      "img-src 'self' data: https:",
      "connect-src 'self'"
    ].join('; ')
  }];

  // Permissions Policy — disable unused browser features
  headers['permissions-policy'] = [{
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=(), payment=()'
  }];

  return response;
};

This viewer-response function adds seven security headers to every response. Because it runs at the edge on both cache hits and misses, you get consistent security header coverage without modifying your origin application. See the AWS security best practices guide and our WAF and Shield guide for complementary perimeter security controls.

Note: If your CloudFront distribution already has a Response Headers Policy attached to the behaviour, any headers set by that policy will override headers set by Lambda@Edge. Remove conflicting headers from the policy or rely entirely on Lambda@Edge to avoid confusion.

8. Use Case 4: Authentication at the Edge

Protecting private content by validating authentication credentials at the CloudFront edge — before the request reaches your origin — reduces load on origin compute and prevents unauthenticated traffic from consuming origin resources. This is particularly valuable for protecting S3-hosted private assets, internal tools, and premium content behind a paywall.

The following viewer-request handler validates a JWT passed in a cookie. It decodes the token's claims without making an external network call (viewer-request has a 5-second timeout and minimal memory, so keeping the check local is important). For full cryptographic verification you would need to either embed the public key in the function or use origin-request (which supports network calls) to call a validation endpoint or Amazon Cognito.

'use strict';

// Embedded base64url decode (no external dependencies needed)
function base64urlDecode(str) {
  const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  const padded = base64 + '=='.slice(0, (4 - base64.length % 4) % 4);
  return Buffer.from(padded, 'base64').toString('utf8');
}

function parseJwt(token) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    return JSON.parse(base64urlDecode(parts[1]));
  } catch (e) {
    return null;
  }
}

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // Extract JWT from cookie
  let token = null;
  if (headers['cookie']) {
    for (const cookie of headers['cookie']) {
      const match = cookie.value.match(/auth_token=([^;]+)/);
      if (match) { token = match[1]; break; }
    }
  }

  // Also accept Bearer token in Authorization header
  if (!token && headers['authorization']) {
    const authHeader = headers['authorization'][0].value;
    if (authHeader.startsWith('Bearer ')) {
      token = authHeader.substring(7);
    }
  }

  if (!token) {
    return {
      status: '401',
      statusDescription: 'Unauthorized',
      headers: {
        'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Bearer' }],
        'content-type': [{ key: 'Content-Type', value: 'application/json' }]
      },
      body: JSON.stringify({ error: 'Authentication required' })
    };
  }

  const claims = parseJwt(token);
  if (!claims) {
    return {
      status: '403',
      statusDescription: 'Forbidden',
      headers: { 'content-type': [{ key: 'Content-Type', value: 'application/json' }] },
      body: JSON.stringify({ error: 'Invalid token' })
    };
  }

  // Check token expiry
  const now = Math.floor(Date.now() / 1000);
  if (claims.exp && claims.exp < now) {
    return {
      status: '401',
      statusDescription: 'Unauthorized',
      headers: { 'content-type': [{ key: 'Content-Type', value: 'application/json' }] },
      body: JSON.stringify({ error: 'Token expired' })
    };
  }

  // Forward user identity to origin as a custom header
  request.headers['x-user-id'] = [{ key: 'X-User-Id', value: claims.sub || '' }];
  request.headers['x-user-email'] = [{ key: 'X-User-Email', value: claims.email || '' }];

  return request;
};

For production use, cryptographic JWT signature verification (RS256 or HS256) is essential. Because viewer-request functions cannot make outbound network calls, embed the JWKS public key material directly in the function package using a library like jsonwebtoken bundled via webpack or esbuild. Rotate the embedded key by redeploying the function and updating the CloudFront association. For a fully managed approach, integrate with Amazon Cognito hosted UI and use Cognito's JWKs endpoint from an origin-request function.

9. Use Case 5: Image Resizing on the Fly

Serving appropriately sized images is one of the highest-impact performance optimisations you can make. A desktop hero image served to a mobile device wastes bandwidth and degrades Core Web Vitals scores. Lambda@Edge on the origin-response event (which has up to 10 GB of memory and 30 seconds of execution time) can intercept image responses from your S3 origin, resize them using the Sharp library, and return the resized image — which CloudFront then caches for subsequent requests at that size.

The architecture works as follows: the client appends query parameters like ?w=400&h=300&fmt=webp to image URLs. A CloudFront Function (or Lambda@Edge viewer-request) normalises and validates these parameters and adds them to the cache key. On a cache miss, the origin-request function fetches the original image from S3, while the origin-response function performs the resize before caching. This example shows the origin-response resize handler:

'use strict';
const AWS = require('aws-sdk');
const Sharp = require('sharp');

const S3 = new AWS.S3({ region: 'us-east-1' });
const BUCKET = 'my-image-bucket';

exports.handler = async (event) => {
  const { request, response } = event.Records[0].cf;

  // Only process image responses
  const contentType = response.headers['content-type'];
  if (!contentType || !contentType[0].value.startsWith('image/')) {
    return response;
  }

  // Parse requested dimensions from the query string
  const params = new URLSearchParams(request.querystring);
  const width  = parseInt(params.get('w') || '0', 10) || null;
  const height = parseInt(params.get('h') || '0', 10) || null;
  const format = ['webp', 'jpeg', 'png', 'avif'].includes(params.get('fmt'))
    ? params.get('fmt')
    : 'webp';

  if (!width && !height) return response; // Nothing to resize

  // Fetch original from S3 (response body is base64 encoded by CloudFront)
  const originalKey = decodeURIComponent(request.uri.slice(1)); // strip leading /
  const s3Object = await S3.getObject({ Bucket: BUCKET, Key: originalKey }).promise();

  // Resize with Sharp
  let pipeline = Sharp(s3Object.Body).rotate(); // auto-rotate from EXIF
  if (width || height) {
    pipeline = pipeline.resize(width, height, {
      fit: 'cover',
      withoutEnlargement: true
    });
  }
  const resizedBuffer = await pipeline[format]({ quality: 82 }).toBuffer();

  // Return the resized image
  const mimeMap = { webp: 'image/webp', jpeg: 'image/jpeg', png: 'image/png', avif: 'image/avif' };
  return {
    status: '200',
    statusDescription: 'OK',
    headers: {
      'content-type': [{ key: 'Content-Type', value: mimeMap[format] }],
      'cache-control': [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
      'vary': [{ key: 'Vary', value: 'Accept' }]
    },
    bodyEncoding: 'base64',
    body: resizedBuffer.toString('base64')
  };
};

The Sharp library must be compiled for the Lambda execution environment (Amazon Linux 2). Use a Docker build or the --platform linux/arm64 flag with npm to produce the correct native binary. The function package including Sharp is typically 10–15 MB zipped, well within Lambda@Edge's 50 MB limit. On first invocation for a given size, the function fetches from S3 and resizes; CloudFront then caches the result, so subsequent requests for the same image at the same size are served directly from the edge with zero compute cost.

10. Debugging Lambda@Edge

Debugging Lambda@Edge is more complex than debugging standard Lambda because your function executes across dozens of geographically distributed edge locations simultaneously. There is no single CloudWatch log group — logs are written to the region where the edge location is located, not the region where the function was deployed.

If a user in Sydney triggers your function, the logs appear in ap-southeast-2. A user in Frankfurt triggers logs in eu-central-1. The log group name follows the pattern /aws/lambda/us-east-1.<function-name> and appears in each region that has served at least one invocation. To find where your logs are, search for the log group name across all regions using the CloudWatch console's cross-region search, or use the CLI:

# List all regions where your function has written logs
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  aws logs describe-log-groups \
    --region "$region" \
    --log-group-name-prefix "/aws/lambda/us-east-1.my-edge-function" \
    --query 'logGroups[].logGroupName' \
    --output text 2>/dev/null
done

For structured tracing, enable AWS X-Ray on the Lambda@Edge function. X-Ray traces propagate through CloudFront and show the full request lifecycle — viewer to edge function to origin — with timing breakdowns. Enable it in the Lambda console under "Monitoring and operations tools" and set the tracing mode to "Active".

Common pitfall: Lambda@Edge functions cannot use environment variables. Any configuration (e.g., allowed origins, feature flags, secret keys) must be hardcoded in the function package or fetched from an AWS service like SSM Parameter Store (origin-request/response only, not viewer events). Use a CI/CD pipeline to inject configuration at build time.

When a Lambda@Edge function throws an unhandled exception, CloudFront returns a 502 Bad Gateway error to the viewer. Enable CloudFront access logging to capture these errors, and set up a CloudWatch alarm on the 5xxErrorRate metric for your distribution. Test your functions locally using the aws-lambda-local package or the Lambda console's test feature before associating them with a live CloudFront distribution.

11. Limits and Quotas

Lambda@Edge has stricter limits than standard regional Lambda, and several of these limits are not adjustable via a service quota increase request. Understanding them before you design your edge logic will save you from painful rearchitecting later.

LimitViewer EventsOrigin Events
Maximum execution timeout5 seconds30 seconds
Maximum memory128 MB10,240 MB (10 GB)
Deployment package size (zipped)50 MB
Deployment package size (unzipped)250 MB
Response body size (generated)40 KB1 MB
Request body size (readable)40 KB (viewer-req), 1 MB (origin-req)
Header value size10 KB per header
Number of headers100 per request/response
Concurrent executions10,000 (adjustable)
Functions per CloudFront event1
Environment variablesNot supported
LayersNot supported
VPCNot supported
Container image deploymentNot supported

The lack of environment variable support and Lambda Layers means all dependencies must be bundled into the deployment package. Use a build tool like esbuild or webpack to create a single minified bundle that includes your handler and all dependencies. For the Sharp image library (which has native binaries), use a Docker environment matching the Lambda execution environment to compile the binaries before packaging.

The 5-second timeout on viewer events is a hard constraint. If your viewer-request function makes any external network call that takes longer than 5 seconds, the request will fail with a 503 error visible to the user. Design viewer-event functions to be purely computational — move any I/O to origin events or pre-compute and embed data in the function package.

The cold start behaviour of Lambda@Edge is similar to standard Lambda but occurs independently at each edge location. A newly deployed function or one that has not received traffic in a particular region for several minutes will experience a cold start. The lightweight nature of most edge functions (simple header manipulation, JWT parsing) keeps cold start durations typically under 100 ms, but bundling large dependencies like Sharp can push cold starts to 500 ms or more.

12. Cost Model vs Standard Lambda

Lambda@Edge pricing is higher per invocation than standard Lambda but is often far cheaper than the alternative of routing all traffic to a centralised origin. Understanding the pricing dimensions helps you estimate costs and choose between Lambda@Edge and CloudFront Functions for appropriate workloads.

Lambda@Edge charges on two dimensions: request count and compute duration (GB-seconds). As of 2026, the pricing is:

  • Requests: $0.60 per 1 million Lambda@Edge requests (for both viewer and origin events)
  • Duration: $0.00000625001 per 128 MB-second (billed in 1 ms increments)
  • Free tier: 1 million requests and 400,000 GB-seconds per month (shared with standard Lambda)

For comparison, CloudFront Functions cost $0.10 per 1 million requests with no separate compute charge — six times cheaper for simple viewer-event operations. For a high-traffic site processing 100 million viewer-request events per month:

  • CloudFront Functions: $10/month
  • Lambda@Edge at 1 ms average duration (128 MB): $60 (requests) + $0.80 (compute) ≈ $61/month

However, Lambda@Edge origin-request functions only execute on cache misses. If your cache hit rate is 90%, origin-request invocations are one-tenth of total requests, making Lambda@Edge highly economical for origin-event use cases. A site with 100 million requests/month and a 90% cache hit rate pays for only 10 million origin-request invocations: $6/month in request charges plus a few dollars in compute.

Cost optimisation strategy: Use CloudFront Functions for viewer-request/response operations (header manipulation, simple redirects, cache key normalisation). Reserve Lambda@Edge for origin events or complex viewer-event logic that requires more than 1 ms or 10 KB of code. This combination gives you the full power of Lambda@Edge where it matters while keeping viewer-event costs minimal.

There are no additional data transfer charges specific to Lambda@Edge — data transfer between Lambda@Edge and CloudFront edge locations is free. You only pay the standard CloudFront data transfer rates for responses sent to viewers. Overall, Lambda@Edge's cost model is very competitive when you factor in the elimination of origin infrastructure that would otherwise be needed to handle the same operations at scale.

Read Next