
Lambda Web Functions の MultiRegion エンドポイントは遅延が最小のリージョンに届くのか試してみた
はじめに
2026年10月に GA となった Lambda Web Functions は、HTTP リクエストを直接受け取って処理する新しいタイプの Lambda 関数です。
Lambda Web Functions には、複数リージョンへ複製した関数に 1 つのグローバル URL で届く MultiRegion エンドポイントがあります。
MultiRegion は遅延ベースのルーティングで、Route 53 の遅延計測に基づき、呼び出し元にとって通常最も遅延が小さいリージョンへ振り分けるとされています。
この記事では、Route 53 ヘルスチェッカー 8 地点と CloudShell 3 か所から、グローバル URL がどのリージョンの Lambda に届くかを実測しました。
検証の構成
ap-northeast-1、us-east-1、us-east-2、us-west-2、eu-west-1 の 5 リージョンに同じアプリを置きました。アプリは /ip/ で接続元の IP、実行リージョン、リクエスト ID を JSON で返し、同じ内容を Lambda のログにも出力します。デプロイには cdkd 0.299.3 を使いました。
ヘルスチェックは、グローバル URL に 1 本、5 つのリージョン別 URL(PerRegion エンドポイントでリージョンごとに発行される URL)にそれぞれ 1 本作りました。設定はどれも同じで、HTTPS、ポート 443、パス /ip/、間隔 30 秒、SNI 有効、レイテンシー測定ありです。チェッカーは選べる 8 リージョンすべてを指定し、各 5 分間動かしました。レイテンシーの比較には TTFB(最初の 1 バイトまでの時間)を使います。
利用者の体験に近い実転送時間を測るため、東京、バージニア、フランクフルトの CloudShell からも計測しました。グローバル URL と 5 つのリージョン別 URL に curl を実行し、グローバル URL のドメインを dig で引きました。
Route 53 ヘルスチェッカーから見た振り分け
チェッカー 8 地点について、グローバル URL の応答先と TTFB、5 つのリージョン別 URL の TTFB を並べます。単位はミリ秒で、各値は CloudWatch の 60 秒平均 5 点の平均です。
応答先は、5 リージョンの Lambda のログで確かめました。ログに出る送信元 IP を、aws route53 get-health-check-status が返すチェッカーのリージョンと IP の一覧に当てはめます。どのチェッカーのリクエストがどのリージョンの Lambda に届いたかを数えています。
| ヘルスチェッカー | グローバル URL の応答先 | グローバル 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 |
表のとおり、どの地点でも TTFB が最も短いリージョンが応答していました。各チェッカーのリクエストは 5 分間ずっと同じリージョンに届いています。グローバル URL のリクエストのうち、us-east-2 に届いたものは 0 件でした。
チェッカーと同じリージョンにデプロイ先がある 4 地点では、グローバル URL の TTFB は 30 ミリ秒台でした。us-west-1 は近くの us-west-2 に届き、TTFB は 119.9 ミリ秒でした。5 リージョンのどこからも遠い sa-east-1、ap-southeast-1、ap-southeast-2 は、TTFB が最も短いリージョンに届いても数百ミリ秒かかりました。
CloudShell から見た振り分け
curl の応答先と実転送時間
東京(ap-northeast-1)、バージニア(us-east-1)、フランクフルト(eu-central-1)の CloudShell から各 URL に curl を実行しました。ウォームアップを 1 回行ったあと、1 秒おきに 10 回測定しています。表は curl の実転送時間(time_total)の 10 回の平均で、単位はミリ秒です。応答先は Lambda のログで確認しました。
| 測定場所 | グローバル URL の応答先 | グローバル URL | ap-northeast-1 | us-east-1 | us-east-2 | us-west-2 | eu-west-1 |
|---|---|---|---|---|---|---|---|
| 東京 | ap-northeast-1 | 25.1 | 27.0 | 475.1 | 426.7 | 322.1 | 635.4 |
| バージニア | us-east-1 | 35.6 | 461.6 | 34.3 | 56.0 | 213.0 | 227.4 |
| フランクフルト | eu-west-1 | 81.0 | 708.2 | 298.1 | 306.9 | 451.7 | 83.0 |
3 か所とも、グローバル URL の実転送時間は、応答先リージョンのリージョン別 URL とほぼ同じでした。フランクフルトにはデプロイ先がなく、5 リージョンのうち実転送時間が最も短い eu-west-1 に届きました。
DNS の応答
同じグローバル URL のドメインを CloudShell から dig で引くと、CNAME の宛先リージョンが場所ごとに変わり、curl の応答先と一致しました。
$ 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
コード全文
全ファイル
デプロイと撤去の手順です。build-app.sh で app-src/ip.ts を app-ip/index.js にバンドルしてから、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
まとめ
Lambda Web Functions のグローバル URL の応答先を、Route 53 ヘルスチェッカー 8 地点と CloudShell 3 か所から測定しました。そのすべてで、応答先は遅延が最小のリージョンでした。
グローバルに展開する Web サイトで低レイテンシな応答が必要な場合は、Lambda Web Functions を複数リージョンに配置し、グローバル URL をお試しください。








