I Tried Out Whether a Multi-Region Endpoint for Lambda Web Functions Reaches the Region with the Lowest Latency

I Tried Out Whether a Multi-Region Endpoint for Lambda Web Functions Reaches the Region with the Lowest Latency

I used Route 53 health checkers and CloudShell to test whether AWS Lambda Web Functions' global URL reaches the lowest-latency region. At every measured location, the lowest-latency region responded, with routing done via DNS responses.
2026.10.14

This page has been translated by machine translation. View original

Introduction

Lambda Web Functions, which became GA in October 2026, is a new type of Lambda function that receives and processes HTTP requests directly.

https://dev.classmethod.jp/articles/aws-lambda-web-functions-ga-express/

Lambda Web Functions has a MultiRegion endpoint that lets a single global URL reach functions replicated across multiple Regions.

https://aws.amazon.com/jp/blogs/aws/announcing-aws-lambda-web-functions-for-fast-consistent-web-app-performance-anywhere/

MultiRegion uses latency-based routing; based on Route 53 latency measurements, it is said to route requests to the Region that normally has the lowest latency for the caller.

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-endpoint-replication.html

In this article, I measured which Region's Lambda the global URL actually reaches, from 8 Route 53 health checker locations and 3 CloudShell locations.

Test Setup

I placed the same app in 5 Regions: ap-northeast-1, us-east-1, us-east-2, us-west-2, and eu-west-1. At /ip/, the app returns the source IP, the execution Region, and the request ID as JSON, and it outputs the same content to the Lambda logs. I used cdkd 0.299.3 for deployment.

I created one health check for the global URL and one for each of the 5 Region-specific URLs (URLs issued per Region by the PerRegion endpoint). All settings were the same: HTTPS, port 443, path /ip/, interval 30 seconds, SNI enabled, and latency measurement enabled. I specified all 8 selectable checker Regions and ran each for 5 minutes. For latency comparison, I use TTFB (time to first byte).

To measure actual transfer time closer to the user experience, I also measured from CloudShell in Tokyo, Virginia, and Frankfurt. I ran curl against the global URL and the 5 Region-specific URLs, and resolved the global URL's domain with dig.

Routing as Seen from the Route 53 Health Checkers

For the 8 checker locations, I list the destination that responded to the global URL and its TTFB, along with the TTFB for the 5 Region-specific URLs. The unit is milliseconds, and each value is the average of five 60-second CloudWatch averages.

I confirmed the responding destination from the logs of the Lambda functions in the 5 Regions. I matched the source IPs in the logs against the list of checker Regions and IPs returned by aws route53 get-health-check-status. I counted which checker's requests reached which Region's Lambda.

Health checker Global URL destination Global URL ap-northeast-1 us-east-1 us-east-2 us-west-2 eu-west-1
ap-northeast-1 ap-northeast-1 32.7 26.7 770.6 697.2 523.7 1029.1
ap-southeast-1 ap-northeast-1 371.5 363.6 1113.6 1050.4 883.1 888.9
ap-southeast-2 ap-northeast-1 556.4 569.2 1052.1 953.2 730.4 1289.9
us-east-1 us-east-1 37.0 747.7 30.1 76.4 320.9 382.0
us-west-1 us-west-2 119.9 561.4 317.6 264.9 115.7 665.1
us-west-2 us-west-2 32.0 509.9 308.1 274.1 28.7 606.0
eu-west-1 eu-west-1 36.8 1031.0 365.2 419.3 603.7 29.9
sa-east-1 us-east-1 583.8 1308.2 578.0 631.2 882.9 892.4

As the table shows, at every location the Region with the shortest TTFB responded. Each checker's requests reached the same Region throughout the 5 minutes. None of the requests to the global URL reached us-east-2.

At the 4 locations that have a deployment in the same Region as the checker, the global URL's TTFB was in the 30 ms range. us-west-1 reached the nearby us-west-2, with a TTFB of 119.9 ms. For sa-east-1, ap-southeast-1, and ap-southeast-2, which are far from all 5 Regions, it took several hundred milliseconds even when reaching the Region with the shortest TTFB.

Routing as Seen from CloudShell

curl Destinations and Actual Transfer Times

I ran curl against each URL from CloudShell in Tokyo (ap-northeast-1), Virginia (us-east-1), and Frankfurt (eu-central-1). After one warm-up request, I measured 10 times at 1-second intervals. The table shows the average of the 10 curl actual transfer times (time_total), in milliseconds. I confirmed the responding destination from the Lambda logs.

Measurement location Global URL destination Global URL ap-northeast-1 us-east-1 us-east-2 us-west-2 eu-west-1
Tokyo ap-northeast-1 25.1 27.0 475.1 426.7 322.1 635.4
Virginia us-east-1 35.6 461.6 34.3 56.0 213.0 227.4
Frankfurt eu-west-1 81.0 708.2 298.1 306.9 451.7 83.0

At all 3 locations, the global URL's actual transfer time was almost the same as that of the Region-specific URL of the responding Region. Frankfurt has no deployment, and requests reached eu-west-1, which had the shortest actual transfer time among the 5 Regions.

DNS Responses

When I resolved the same global URL domain with dig from CloudShell, the CNAME destination Region changed by location and matched the curl destination.

$ dig y5pf7t5w6zd3kjn6.lambda-web.global.on.aws +noall +answer

# 東京
y5pf7t5w6zd3kjn6.lambda-web.global.on.aws. 300 IN CNAME y5pf7t5w6zd3kjn6.r8m.lambda-web.ap-northeast-1.on.aws.
y5pf7t5w6zd3kjn6.r8m.lambda-web.ap-northeast-1.on.aws. 300 IN CNAME c300.ap-northeast-1.prod.frontend.lambda-lite.aws.dev.
c300.ap-northeast-1.prod.frontend.lambda-lite.aws.dev. 13 IN A 18.177.191.50

# バージニア
y5pf7t5w6zd3kjn6.lambda-web.global.on.aws. 300 IN CNAME y5pf7t5w6zd3kjn6.r8m.lambda-web.us-east-1.on.aws.
y5pf7t5w6zd3kjn6.r8m.lambda-web.us-east-1.on.aws. 300 IN CNAME c300.us-east-1.prod.frontend.lambda-lite.aws.dev.
c300.us-east-1.prod.frontend.lambda-lite.aws.dev. 22 IN A 52.22.111.67

# フランクフルト
y5pf7t5w6zd3kjn6.lambda-web.global.on.aws. 300 IN CNAME y5pf7t5w6zd3kjn6.r8m.lambda-web.eu-west-1.on.aws.
y5pf7t5w6zd3kjn6.r8m.lambda-web.eu-west-1.on.aws. 300 IN CNAME c300.eu-west-1.prod.frontend.lambda-lite.aws.dev.
c300.eu-west-1.prod.frontend.lambda-lite.aws.dev. 60 IN A 54.195.200.219

Full Code

All files

These are the steps for deployment and teardown. Bundle app-src/ip.ts into app-ip/index.js with build-app.sh, then deploy the stacks with cdkd.

npm install
./build-app.sh

# ヘルスチェック用(グローバル URL / リージョン別 URL)
npx cdkd deploy WebFnHcGlobal --yes
npx cdkd deploy WebFnHcRegional --yes
# CloudShell の curl・dig 用
npx cdkd deploy WebFnCurl --yes

# 測定後に撤去する(ヘルスチェックは残すと課金が続く)
npx cdkd destroy --all --force

package.json

{
  "name": "web-fn-global-hc",
  "version": "0.0.1",
  "private": true,
  "type": "commonjs",
  "dependencies": {
    "@go-to-k/cdkd": "0.299.3",
    "aws-cdk": "2.1145.0",
    "aws-cdk-lib": "2.273.0",
    "constructs": "10.5.0",
    "esbuild": "0.25.0",
    "typescript": "5.6.3",
    "ts-node": "10.9.2",
    "@types/node": "22.9.0"
  }
}

cdk.json

{ "app": "npx ts-node --prefer-ts-exts bin/app.ts", "context": { "@aws-cdk/core:newStyleStackSynthesis": true } }

tsconfig.json

{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "cdk.out-ts" }, "include": ["bin", "lib"] }

lib/hc-stacks.ts

import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as r53 from 'aws-cdk-lib/aws-route53';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import { Construct } from 'constructs';

// Regions to replicate the function to
const FN_REGIONS = ['ap-northeast-1', 'us-east-1', 'us-east-2', 'us-west-2', 'eu-west-1'];
// Regions of the Route 53 health checkers
const CHECKER_REGIONS = ['us-east-1', 'us-west-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'sa-east-1'];

abstract class BaseStack extends cdk.Stack {
  protected makeFunction(functionName: string, endpointType: 'MultiRegion' | 'PerRegion') {
    const { fn, revision } = this.makeFunctionBase(functionName);
    return this.makeEndpoint('Endpoint', 'main', endpointType, fn, revision);
  }

  protected makeEndpoint(id: string, endpointName: string, endpointType: 'MultiRegion' | 'PerRegion', fn: lambda.CfnWebFunction, revision: lambda.CfnWebFunctionRevision) {
    return new lambda.CfnWebFunctionEndpoint(this, id, {
      functionName: fn.ref,
      endpointName,
      endpointType,
      regions: FN_REGIONS, // the home Region is added automatically; listing it is allowed
      authType: 'ApplicationManaged', // no auth: the URLs are public. Delete the stacks after the test
      revisionWeights: [{ revisionId: revision.attrRevisionId, weight: 100 }],
    });
  }

  protected makeFunctionBase(functionName: string) {
    // Code bucket: versioning + bucket policy for lambda.amazonaws.com (Web Functions requirement)
    const codeBucket = new s3.Bucket(this, 'CodeBucket', {
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });
    codeBucket.addToResourcePolicy(new iam.PolicyStatement({
      principals: [new iam.ServicePrincipal('lambda.amazonaws.com')],
      actions: ['s3:GetObject', 's3:GetObjectVersion'],
      resources: [codeBucket.arnForObjects('*')],
      conditions: { ArnLike: { 'aws:SourceArn': `arn:${this.partition}:lambda:${this.region}:${this.account}:web-function/*` } },
    }));
    const execRole = new iam.Role(this, 'ExecRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
    });
    const deployment = new s3deploy.BucketDeployment(this, 'Code', {
      sources: [s3deploy.Source.asset('app-ip')],
      destinationBucket: codeBucket,
      extract: false,
      prune: false,
    });
    const fn = new lambda.CfnWebFunction(this, 'Fn', { functionName });
    const revision = new lambda.CfnWebFunctionRevision(this, 'Revision', {
      functionName: fn.ref,
      buildConfig: {
        codeConfig: {
          s3Object: {
            bucket: codeBucket.bucketName,
            key: cdk.Fn.select(0, deployment.objectKeys),
            versionId: cdk.Fn.select(0, deployment.objectVersionIds),
          },
        },
        runtimeConfig: { runtime: 'nodejs24.x' },
      },
      serviceConfig: { executionRoleArn: execRole.roleArn, timeoutSeconds: 30 },
    });
    revision.node.addDependency(codeBucket.policy!);
    return { fn, revision };
  }

  protected makeHealthCheck(id: string, domainName: string) {
    return new r53.CfnHealthCheck(this, id, {
      healthCheckConfig: {
        type: 'HTTPS',
        fullyQualifiedDomainName: domainName,
        port: 443,
        resourcePath: '/ip/',
        requestInterval: 30,
        failureThreshold: 3,
        measureLatency: true,
        enableSni: true,
        regions: CHECKER_REGIONS,
      },
      healthCheckTags: [{ key: 'Name', value: `${this.stackName}-${id}` }],
    });
  }
}

// Global URL: one MultiRegion endpoint and one health check against it
export class GlobalStack extends BaseStack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    const endpoint = this.makeFunction('webfn-hc-global', 'MultiRegion');
    const hc = this.makeHealthCheck('HcGlobal', endpoint.attrDomainName);
    new cdk.CfnOutput(this, 'GlobalDomain', { value: endpoint.attrDomainName });
    new cdk.CfnOutput(this, 'HealthCheckId', { value: hc.attrHealthCheckId });
  }
}

// Per-Region URLs: one PerRegion endpoint and one health check per Region URL
export class RegionalStack extends BaseStack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    const endpoint = this.makeFunction('webfn-hc-regional', 'PerRegion');
    for (const r of FN_REGIONS) {
      const domain = cdk.Fn.getAtt(endpoint.logicalId, `RegionalEndpoints.${r}.DomainName`).toString();
      const hc = this.makeHealthCheck(`Hc${r.replace(/-/g, '')}`, domain);
      new cdk.CfnOutput(this, `Domain${r.replace(/-/g, '')}`, { value: domain });
      new cdk.CfnOutput(this, `HealthCheckId${r.replace(/-/g, '')}`, { value: hc.attrHealthCheckId });
    }
  }
}

// CloudShell: measure with curl/dig. No health checks.
// ONE function with BOTH a MultiRegion endpoint (global URL) and a PerRegion endpoint (per-Region URLs).
export class CurlStack extends BaseStack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    const { fn, revision } = this.makeFunctionBase('webfn-curl');
    const global = this.makeEndpoint('EndpointGlobal', 'global', 'MultiRegion', fn, revision);
    const regional = this.makeEndpoint('EndpointRegional', 'regional', 'PerRegion', fn, revision);
    regional.addDependency(global); // create one after the other
    new cdk.CfnOutput(this, 'GlobalDomain', { value: global.attrDomainName });
    for (const r of FN_REGIONS) {
      new cdk.CfnOutput(this, `Domain${r.replace(/-/g, '')}`, {
        value: cdk.Fn.getAtt(regional.logicalId, `RegionalEndpoints.${r}.DomainName`).toString(),
      });
    }
  }
}

bin/app.ts

import * as cdk from 'aws-cdk-lib';
import { CurlStack, GlobalStack, RegionalStack } from '../lib/hc-stacks';

const app = new cdk.App();
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'ap-northeast-1' };
// MultiRegion endpoint (one global URL) + 1 health check
new GlobalStack(app, 'WebFnHcGlobal', { env });
// PerRegion endpoints (one URL per Region) + 5 health checks
new RegionalStack(app, 'WebFnHcRegional', { env });
// One function, MultiRegion + PerRegion endpoints, for curl/dig from CloudShell (no health checks)
new CurlStack(app, 'WebFnCurl', { env });

app-src/ip.ts

import http from 'node:http';

// Web Function: returns the caller's IP (last value of X-Forwarded-For) and the Region that served the request.
// The same content is written to the log as one JSON line. Port 3000 is where Lambda sends requests.
const port = Number(process.env.LOCAL_PORT ?? 3000);

http.createServer((req, res) => {
  const xff = String(req.headers['x-forwarded-for'] ?? '');
  const ip = xff.split(',').map((s) => s.trim()).filter(Boolean).pop() ?? '';
  const body = {
    ip,
    region: process.env.AWS_REGION ?? '',
    requestId: String(req.headers['lambda-web-request-id'] ?? ''),
  };
  console.log(JSON.stringify({
    ...body,
    path: req.url,
    host: req.headers.host,
    userAgent: req.headers['user-agent'],
    xForwardedFor: xff,
  }));
  res.setHeader('content-type', 'application/json');
  res.end(JSON.stringify(body));
}).listen(port, () => console.log(`listening on port ${port}`));

build-app.sh

#!/bin/bash
# Bundle app-src/ip.ts into app-ip/index.js (CJS) inside Docker.
set -euo pipefail
cd "$(dirname "$0")"
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD:/app" -w /app node:22 \
  npx esbuild app-src/ip.ts --bundle --platform=node --target=node22 --format=cjs --outfile=app-ip/index.js

Summary

I measured the destinations of the Lambda Web Functions global URL from 8 Route 53 health checker locations and 3 CloudShell locations. In every case, the destination was the Region with the lowest latency.

If you need low-latency responses for a globally deployed website, try placing Lambda Web Functions in multiple Regions and using the global URL.

Share this article

AWSのお困り事はクラスメソッドへ