Lambda Web Functions の MultiRegion エンドポイントは遅延が最小のリージョンに届くのか試してみた

Lambda Web Functions の MultiRegion エンドポイントは遅延が最小のリージョンに届くのか試してみた

AWS Lambda Web Functions のグローバル URL が遅延が最小のリージョンに届くかを、Route 53 ヘルスチェッカーと CloudShell で測りました。測った地点のすべてで遅延が最小のリージョンが応答し、振り分けは DNS の応答で行われていました。
2026.10.14

はじめに

2026年10月に GA となった Lambda Web Functions は、HTTP リクエストを直接受け取って処理する新しいタイプの Lambda 関数です。

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

Lambda Web Functions には、複数リージョンへ複製した関数に 1 つのグローバル URL で届く MultiRegion エンドポイントがあります。

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

MultiRegion は遅延ベースのルーティングで、Route 53 の遅延計測に基づき、呼び出し元にとって通常最も遅延が小さいリージョンへ振り分けるとされています。

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

この記事では、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 をお試しください。

この記事をシェアする

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

関連記事