luizmachado.dev

PT EN

Session 025 — Lambda@Edge vs CloudFront Functions: use cases and limits

Estimated duration: 60 minutes
Prerequisites: session-024-lambda-observabilidade-xray-insights


Objective

By the end, you will be able to choose between Lambda@Edge and CloudFront Functions for a given requirement (based on execution latency, body access, number of regions, cost and deploy time), implement a simple header injection with CloudFront Functions, and understand the timeout and memory limits of each option.


Context

[FACT] CloudFront is AWS's CDN (Content Delivery Network). It distributes content from over 600 points of presence (PoPs) around the world, called edge locations. When a request reaches CloudFront, it is processed at the edge location closest to the user — not in the AWS region of the origin. This means any logic executed at the edge has radically lower latency than a call back to the origin.

[FACT] Two services allow running code at the CloudFront edge: CloudFront Functions (launched in 2021) and Lambda@Edge (launched in 2017). They differ fundamentally in capability, latency, cost and use cases. They are not direct competitors — each solves a different class of problems.

[CONSENSUS] The general rule adopted by most production architectures: if the logic is simple (header manipulation, URL rewrites, cache key normalization) and runs on every request, use CloudFront Functions. If the logic is complex (body access, calls to external services, full JWT authentication, database-based decisions), use Lambda@Edge. The cost of CloudFront Functions is approximately 1/6 of Lambda@Edge per request.


Main concepts

1. The four CloudFront interception points

[FACT] CloudFront intercepts the HTTP flow at four distinct points. Each point has different characteristics and supports different types of edge functions:

Usuário
   │
   ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                         CLOUDFRONT EDGE LOCATION                            │
│                                                                              │
│  ① viewer-request                              ② viewer-response            │
│  Antes do cache check                          Antes de retornar ao usuário  │
│  (CloudFront Functions ✓)                      (CloudFront Functions ✓)      │
│  (Lambda@Edge ✓)                               (Lambda@Edge ✓)              │
│         │                                              ▲                    │
│         ▼                                              │                    │
│   ┌──────────────┐                           ┌─────────────────┐           │
│   │  Cache hit?  │──── HIT ─────────────────►│ Serve do cache  │           │
│   └──────┬───────┘                           └─────────────────┘           │
│          │ MISS                                                              │
│          ▼                                                                   │
│  ③ origin-request            ④ origin-response                             │
│  Antes de ir à origem        Após receber da origem (e cachear)              │
│  (Lambda@Edge ✓ apenas)      (Lambda@Edge ✓ apenas)                         │
│         │                                    ▲                              │
└─────────┼────────────────────────────────────┼──────────────────────────────┘
          │                                    │
          ▼                                    │
       ORIGEM (S3, ALB, EC2, API Gateway...)───┘

[FACT] CloudFront Functions can only be associated with viewer-request and viewer-response. Lambda@Edge can be associated with all four events. This distinction is fundamental: only Lambda@Edge can intercept and modify requests/responses to the origin.


2. CloudFront Functions — sub-millisecond, massive scale, strict limitations

[FACT] CloudFront Functions executes JavaScript (ES2015+) code in a proprietary runtime — it is not Node.js. It is a lightweight JavaScript engine, with no npm modules, no network access, no asynchronous timers, no require(). The code must be synchronous and finish in less than 1ms of compute time.

Limits and capabilities

[FACT] Documented limits:

┌──────────────────────────────┬──────────────────────────────────────────┐
│ Limite                       │ Valor                                    │
├──────────────────────────────┼──────────────────────────────────────────┤
│ Tempo máximo de execução     │ ~1ms (compute utilization 0-100)        │
│ Memória                      │ 2 MB                                     │
│ Tamanho máximo do código     │ 10 KB                                    │
│ Runtime                      │ JavaScript (ES2015+) — NÃO é Node.js    │
│ Acesso ao body da requisição │ NÃO                                      │
│ Chamadas de rede             │ NÃO                                      │
│ Variáveis de ambiente        │ NÃO (use Key Value Store)                │
│ Acesso a sistema de arquivos │ NÃO                                      │
│ Eventos suportados           │ viewer-request, viewer-response          │
│ Escala                       │ Milhões de req/s instantaneamente        │
│ Deploy                       │ CloudFront nativo (não via Lambda)       │
│ Teste integrado              │ Console CloudFront                       │
└──────────────────────────────┴──────────────────────────────────────────┘

Event structure (CloudFront Functions)

[FACT] The event object has a different structure from Lambda@Edge:

// evento recebido pela função (viewer-request)
{
  "version": "1.0",
  "context": {
    "distributionDomainName": "d111111abcdef8.cloudfront.net",
    "distributionId": "EDFDVBD6EXAMPLE",
    "eventType": "viewer-request",
    "requestId": "4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ=="
  },
  "viewer": {
    "ip": "1.2.3.4"
  },
  "request": {
    "method": "GET",
    "uri": "/index.html",
    "querystring": {
      "cat": { "value": "meow" }
    },
    "headers": {
      "host": { "value": "www.example.com" },
      "accept-language": { "value": "pt-BR,pt;q=0.9" }
    },
    "cookies": {
      "session": { "value": "abc123" }
    }
  }
}

[FACT] The function must return the request object (for viewer-request) or response object (for viewer-response). If it returns a response object in viewer-request, the request is interrupted — it never reaches the cache or the origin.

CloudFront Functions examples

// 1. Injetar Security Headers (viewer-response)
function handler(event) {
    var response = event.response;
    var headers = response.headers;

    headers['strict-transport-security'] = { value: 'max-age=31536000; includeSubdomains; preload' };
    headers['x-content-type-options'] = { value: 'nosniff' };
    headers['x-frame-options'] = { value: 'DENY' };
    headers['x-xss-protection'] = { value: '1; mode=block' };
    headers['referrer-policy'] = { value: 'strict-origin-when-cross-origin' };
    headers['permissions-policy'] = { value: 'camera=(), microphone=(), geolocation=()' };

    return response;
}
// 2. Normalizar cache key — remover query strings irrelevantes (viewer-request)
// Sem isso, "?utm_source=email" e "?utm_source=social" geram cache misses separados
function handler(event) {
    var request = event.request;
    var qs = request.querystring;

    // Remove parâmetros de tracking — não afetam o conteúdo
    var tracking = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'fbclid', 'gclid'];
    tracking.forEach(function(param) { delete qs[param]; });

    return request;
}
// 3. URL rewrite — SPA com todas as rotas servindo index.html (viewer-request)
function handler(event) {
    var request = event.request;
    var uri = request.uri;

    // Se não tem extensão de arquivo, serve index.html
    if (!uri.includes('.')) {
        request.uri = '/index.html';
    }
    return request;
}
// 4. Redirect HTTP → HTTPS e www → apex (viewer-request)
function handler(event) {
    var request = event.request;
    var headers = request.headers;
    var host = headers.host ? headers.host.value : '';

    // Redireciona www para apex
    if (host.startsWith('www.')) {
        return {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: {
                'location': { value: 'https://' + host.slice(4) + request.uri }
            }
        };
    }

    return request;
}
// 5. A/B testing via cookie (viewer-request)
// Atribui usuários novos a uma variante e redireciona para o path correto
function handler(event) {
    var request = event.request;
    var cookies = request.cookies;

    // Usuário já tem variante atribuída
    if (cookies['ab-variant']) {
        var variant = cookies['ab-variant'].value;
        request.uri = '/' + variant + request.uri;
        return request;
    }

    // Atribui nova variante (50/50)
    var variant = Math.random() < 0.5 ? 'a' : 'b';
    request.uri = '/' + variant + request.uri;

    // Retorna response para definir o cookie — próximo request não será atribuído
    return {
        statusCode: 302,
        statusDescription: 'Found',
        headers: {
            'location': { value: request.uri },
            'set-cookie': { value: 'ab-variant=' + variant + '; Path=/; Max-Age=2592000' }
        }
    };
}

3. Lambda@Edge — full power, greater operational complexity

[FACT] Lambda@Edge are regular Lambda functions, with the following specific restrictions for edge execution:

┌─────────────────────────────────┬──────────────────────┬──────────────────────┐
│ Limite                          │ Viewer events        │ Origin events         │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Timeout máximo                  │ 5 segundos           │ 30 segundos           │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Memória máxima                  │ 128 MB               │ 128 MB – 10.240 MB    │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Tamanho do package (comprimido) │ 1 MB                 │ 50 MB                 │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Acesso ao body                  │ Sim (40 KB)          │ Sim (1 MB)            │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Chamadas de rede                │ Sim                  │ Sim                   │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Variáveis de ambiente           │ NÃO                  │ NÃO                   │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Layers Lambda                   │ NÃO                  │ NÃO                   │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Runtime                         │ Node.js, Python      │ Node.js, Python       │
├─────────────────────────────────┼──────────────────────┼──────────────────────┤
│ Deploy                          │ us-east-1 apenas     │ us-east-1 apenas      │
└─────────────────────────────────┴──────────────────────┴──────────────────────┘

[FACT] Lambda@Edge must be deployed in the us-east-1 region. Replication to all edge locations worldwide is done automatically by CloudFront when associating the function with a distribution. You only manage the function in us-east-1.

[FACT] Lambda@Edge does not support environment variables. Configurations that would normally go in environment variables must be hardcoded, fetched from SSM Parameter Store/Secrets Manager at runtime (with caching in the init phase), or injected via CloudFormation during deploy.

[FACT] The execution role of the Lambda@Edge function must have both lambda.amazonaws.com and edgelambda.amazonaws.com as trusted principals:

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

Event structure (Lambda@Edge)

[FACT] The event has a more verbose structure — the data is nested inside Records[0].cf:

# viewer-request event (Python)
{
  "Records": [{
    "cf": {
      "config": {
        "distributionDomainName": "d111111abcdef8.cloudfront.net",
        "distributionId": "EDFDVBD6EXAMPLE",
        "eventType": "viewer-request",
        "requestId": "4TyzHTa..."
      },
      "request": {
        "clientIp": "203.0.113.178",
        "method": "GET",
        "uri": "/picture.jpg",
        "querystring": "size=large&color=red",
        "headers": {
          "host": [{ "key": "Host", "value": "d111111abcdef8.cloudfront.net" }],
          "accept-language": [{ "key": "Accept-Language", "value": "en-US,en;q=0.5" }]
        },
        "body": {  # apenas se include_body=True e método POST/PUT
          "inputTruncated": False,
          "action": "read-only",
          "encoding": "base64",
          "data": "aGVsbG8="
        }
      }
    }
  }]
}

[FACT] The Lambda@Edge function must return an object with the same structure as the request or response (without the Records wrapper). To interrupt the request, return a response object:

# Exemplo: autenticação JWT em viewer-request (Lambda@Edge)
import json
import base64
import hmac
import hashlib

# Cache da chave pública (inicializado no init phase, reusado em warm starts)
_JWT_SECRET = None

def get_secret():
    global _JWT_SECRET
    if _JWT_SECRET is None:
        import boto3
        # NOTA: Lambda@Edge em us-east-1, SSM deve estar em us-east-1 também
        ssm = boto3.client('ssm', region_name='us-east-1')
        _JWT_SECRET = ssm.get_parameter(
            Name='/app/jwt-secret', WithDecryption=True
        )['Parameter']['Value']
    return _JWT_SECRET

def handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']

    # Extrai token do header Authorization
    auth_header = headers.get('authorization', [{}])[0].get('value', '')
    if not auth_header.startswith('Bearer '):
        return unauthorized_response()

    token = auth_header[7:]

    try:
        payload = verify_jwt(token, get_secret())
        # Injeta user_id como header para a origem
        request['headers']['x-user-id'] = [{
            'key': 'X-User-Id',
            'value': payload['sub']
        }]
        return request
    except Exception:
        return unauthorized_response()

def verify_jwt(token, secret):
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError("Invalid JWT")
    header_payload = f"{parts[0]}.{parts[1]}"
    signature = base64.urlsafe_b64decode(parts[2] + '==')
    expected = hmac.new(secret.encode(), header_payload.encode(), hashlib.sha256).digest()
    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid signature")
    payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
    return payload

def unauthorized_response():
    return {
        'status': '401',
        'statusDescription': 'Unauthorized',
        'headers': {
            'www-authenticate': [{ 'key': 'WWW-Authenticate', 'value': 'Bearer' }],
            'content-type': [{ 'key': 'Content-Type', 'value': 'application/json' }]
        },
        'body': json.dumps({ 'error': 'Unauthorized' })
    }

4. Decision table — CloudFront Functions vs Lambda@Edge

[FACT] The official AWS documentation summarizes the decision as:

USE CloudFront Functions quando:             USE Lambda@Edge quando:
──────────────────────────────────────────   ──────────────────────────────────────────
✓ URL normalization e rewrites               ✓ Autenticação/autorização complexa
✓ Manipulação de headers (add/remove/mod)    ✓ Lógica dependente do body da requisição
✓ Normalizar cache keys (remover params)     ✓ Chamadas a AWS (DynamoDB, SSM, etc.)
✓ Cookie manipulation simples                ✓ Resposta diferente por geolocalização
✓ A/B redirect por cookie                   ✓ Processamento de imagens
✓ Redirecionar HTTP → HTTPS                  ✓ Geração de respostas dinâmicas
✓ Injetar security headers                  ✓ Reescrita de URL baseada em dados externos
✓ Custo mínimo (milhões de req/s)           ✓ Interceptar origin-request/response
✓ Deploy instantâneo                        ✓ Modificar request antes de ir à S3/ALB

Comparative cost (approximate):

CloudFront Functions:
  $0.10 por 1.000.000 de invocações

Lambda@Edge:
  $0.60 por 1.000.000 de invocações (viewer events)
  + $0.00000625125 por GB-segundo

Exemplo: 100M requests/mês, 1ms avg:
  CloudFront Functions: $10/mês
  Lambda@Edge:          $60/mês + duração ≈ $65/mês
  Diferença:            ~6,5x mais caro

5. Deploy via CDK

[FACT] Lambda@Edge must be defined in us-east-1. In a multi-stack CDK app, this requires a separate stack deployed in that region:

from aws_cdk import (
    App, Stack, Environment,
    aws_lambda as lambda_,
    aws_cloudfront as cloudfront,
    aws_cloudfront_origins as origins,
    aws_s3 as s3,
    Duration,
)

# ── Stack de Edge Functions (deve estar em us-east-1) ──────────────────────────
class EdgeFunctionsStack(Stack):
    def __init__(self, scope, construct_id, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        # Lambda@Edge — viewer-request para autenticação JWT
        self.auth_function = cloudfront.experimental.EdgeFunction(
            self, "AuthFunction",
            runtime=lambda_.Runtime.PYTHON_3_12,
            handler="auth.handler",
            code=lambda_.Code.from_asset("src/edge/auth"),
            timeout=Duration.seconds(5),
            memory_size=128,
            description="JWT authentication at edge",
            # Não suporta: environment_variables, layers
        )

# ── Stack principal da aplicação ───────────────────────────────────────────────
class AppStack(Stack):
    def __init__(self, scope, construct_id, edge_stack: EdgeFunctionsStack, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        bucket = s3.Bucket(self, "StaticAssets")

        # CloudFront Function — injetar security headers (nativa, não Lambda)
        security_headers_fn = cloudfront.Function(
            self, "SecurityHeadersFn",
            code=cloudfront.FunctionCode.from_inline("""
function handler(event) {
    var response = event.response;
    var headers = response.headers;
    headers['strict-transport-security'] = { value: 'max-age=31536000; includeSubdomains' };
    headers['x-content-type-options'] = { value: 'nosniff' };
    headers['x-frame-options'] = { value: 'DENY' };
    return response;
}
"""),
            function_name="SecurityHeadersInjection",
            comment="Inject security headers on all responses",
        )

        distribution = cloudfront.Distribution(
            self, "Distribution",
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.S3BucketOrigin.with_origin_access_control(bucket),
                viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                cache_policy=cloudfront.CachePolicy.CACHING_OPTIMIZED,
                # Lambda@Edge: autenticação JWT em viewer-request
                edge_lambdas=[
                    cloudfront.EdgeLambda(
                        function_version=edge_stack.auth_function.current_version,
                        event_type=cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
                        include_body=False,
                    )
                ],
                # CloudFront Function: security headers em viewer-response
                function_associations=[
                    cloudfront.FunctionAssociation(
                        function=security_headers_fn,
                        event_type=cloudfront.FunctionEventType.VIEWER_RESPONSE,
                    )
                ],
            ),
        )

# ── App com múltiplas stacks ───────────────────────────────────────────────────
app = App()

edge_stack = EdgeFunctionsStack(
    app, "EdgeFunctionsStack",
    env=Environment(region="us-east-1")   # OBRIGATÓRIO para Lambda@Edge
)

app_stack = AppStack(
    app, "AppStack",
    edge_stack=edge_stack,
    env=Environment(region="sa-east-1")   # Região principal da aplicação
)

app_stack.add_dependency(edge_stack)
app.synth()

[FACT] cloudfront.experimental.EdgeFunction in CDK automatically creates the function in us-east-1 (regardless of the region of the stack where it is used), replicates to edge locations, and configures the correct trust policy with edgelambda.amazonaws.com.


Practical example

Scenario: Content platform with three requirements: (a) all responses must have security headers, (b) authenticated users see personalized content — the origin needs to know the user_id, (c) image files must be served with long cache without analytics parameters polluting the cache.

Architected solution:

viewer-request (CloudFront Function):
  → Remove query params de tracking (utm_*, fbclid)
  → Normaliza cache key

viewer-request (Lambda@Edge — somente paths /api/*):
  → Verifica JWT
  → Injeta X-User-Id no request para a origem

viewer-response (CloudFront Function):
  → Injeta security headers em todos os responses
// cloudfront-function-normalize.js (viewer-request)
// Aplica ao behavior padrão (assets estáticos)
function handler(event) {
    var request = event.request;
    var qs = request.querystring;

    // Remove tracking params que não afetam o conteúdo
    ['utm_source','utm_medium','utm_campaign','utm_content',
     'utm_term','fbclid','gclid','_ga'].forEach(function(p) {
        delete qs[p];
    });

    // Normaliza Accept-Language para pt ou en (reduz variações de cache)
    var lang = request.headers['accept-language'];
    if (lang) {
        var value = lang.value || '';
        request.headers['cf-accept-language'] = {
            value: value.startsWith('pt') ? 'pt' : 'en'
        };
    }

    return request;
}
# lambda_edge_auth.py (viewer-request — behavior /api/*)
import json, os, base64, hmac, hashlib, boto3

_SECRET = None

def _load_secret():
    global _SECRET
    if _SECRET is None:
        ssm = boto3.client('ssm', region_name='us-east-1')
        _SECRET = ssm.get_parameter(
            Name='/plataforma/jwt-secret', WithDecryption=True
        )['Parameter']['Value']
    return _SECRET

def handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request.get('headers', {})
    auth = headers.get('authorization', [{}])[0].get('value', '')

    if not auth.startswith('Bearer '):
        return _deny()

    try:
        payload = _decode_jwt(auth[7:], _load_secret())
        request['headers']['x-user-id'] = [{'key':'X-User-Id','value': payload['sub']}]
        request['headers']['x-user-plan'] = [{'key':'X-User-Plan','value': payload.get('plan','free')}]
        return request
    except Exception:
        return _deny()

def _decode_jwt(token, secret):
    h, p, s = token.split('.')
    sig = base64.urlsafe_b64decode(s + '==')
    expected = hmac.new(secret.encode(), f"{h}.{p}".encode(), hashlib.sha256).digest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError('bad sig')
    return json.loads(base64.urlsafe_b64decode(p + '=='))

def _deny():
    return {
        'status': '401',
        'statusDescription': 'Unauthorized',
        'headers': {'content-type': [{'key':'Content-Type','value':'application/json'}]},
        'body': '{"error":"Unauthorized"}'
    }

Common pitfalls

Pitfall 1 — Using environment variables in Lambda@Edge

The mistake: The developer creates the Lambda@Edge function with environment_variables={"JWT_SECRET": "..."} in CDK. The deploy fails with error: Lambda@Edge does not support environment variables.

Why it happens: Lambda@Edge replicates the function to dozens of global edge locations. Environment variables are regional — there is no mechanism to replicate them along with the code to each PoP.

How to avoid: Three alternatives in order of preference:
1. SSM Parameter Store / Secrets Manager (read in the init phase, cached in a global variable)
2. Hardcode for non-sensitive and static values
3. CloudFront Key Value Store — an AWS managed KV store available in CloudFront Functions (not Lambda@Edge) for lightweight key-value pairs

For Lambda@Edge, option 1 is the production standard: the first invocation (cold start) fetches the secret, subsequent ones (warm) reuse the value cached in memory. The extra cost is one SSM call per cold start.


Pitfall 2 — Deploying in a region other than us-east-1 for Lambda@Edge

The mistake: The developer creates the Lambda function in sa-east-1 (São Paulo) because it is the application's main region, then tries to associate it with CloudFront. The error is: InvalidLambdaFunctionAssociation: The function ARN must be in the same account as the distribution and must be in us-east-1.

Why it happens: The Lambda@Edge replication system is managed from us-east-1. AWS needs a "master" function in us-east-1 to replicate to global edge locations.

How to avoid:
- Use cloudfront.experimental.EdgeFunction in CDK — it ensures deploy in us-east-1 automatically, regardless of the stack's region.
- If using Lambda directly, create the function manually in us-east-1 and reference the ARN with the published version (not $LATEST).
- Never use $LATEST in Lambda@Edge — only published versions are supported.


Pitfall 3 — Forgetting that CloudFront Functions is synchronous JavaScript (not Node.js)

The mistake: The developer writes code with async/await, fetch(), require(), or accesses process.env. The code fails with errors like ReferenceError: fetch is not defined or SyntaxError: Unexpected token.

Why it happens: CloudFront Functions uses a proprietary JavaScript engine (not Node.js). There is no process, no require, no asynchronous I/O. The runtime is intentionally minimalist to guarantee sub-millisecond execution.

How to avoid:
- Synchronous code only — no async, no Promises, no I/O callbacks
- No require() — all code must be in a single file
- No fetch or any network call — if you need external data, use Lambda@Edge
- Always test in the CloudFront console before associating with the distribution — it detects these errors before deploy
- For dynamic configurations, use CloudFront Key Value Store (runtime API: CloudFrontFunction.cf.kvs.get())


Reflection exercise

You are redesigning the edge layer of a video streaming platform. The requirements are: (1) block requests from IPs on a blocklist that is updated daily, (2) add an X-Country header based on CloudFront geolocation, (3) verify if the user has an active plan by querying DynamoDB before serving Premium videos, and (4) add correct cache headers (Cache-Control) on all responses.

Question: For each of the four requirements, would you use CloudFront Functions or Lambda@Edge? On which event (viewer-request, origin-request, etc.)? Justify your choice considering latency, cost, access to external data and cache impact. In particular, for requirement 3, how would you avoid the DynamoDB check happening on every request from an already authenticated user?


Resources for further study

  1. Differences between CloudFront Functions and Lambda@Edge
    URL: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions-choosing.html
    The official comparison table between the two services, with all quantitative limits (timeout, memory, package size, events, body access) and use case recommendations. It is the first page to consult when you need to decide between the two.

  2. Customize at the edge with CloudFront Functions
    URL: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html
    Complete documentation for CloudFront Functions: event structure, JavaScript runtime, how to create and test in the console, Key Value Store, and gallery of usage examples (redirect, header manipulation, A/B testing). Includes the step-by-step tutorial to create your first function.

  3. Customize at the edge with Lambda@Edge
    URL: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html
    Complete guide to Lambda@Edge: the four event types, request and response event structures, how to configure triggers, specific restrictions (us-east-1, no env vars, no layers), and advanced examples (authentication, response generation, body manipulation).