
I Tried Out Whether a Multi-Region Endpoint for Lambda Web Functions Reaches the Region with the Lowest Latency
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.
Lambda Web Functions has a MultiRegion endpoint that lets a single global URL reach functions replicated across multiple Regions.
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.
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.
