@shinyaz

Lambda AZ Metadata with TypeScript Powertools — Same-AZ Routing in 3 Lines

Table of Contents

Introduction

In the previous post, I verified same-AZ routing using Lambda's AZ metadata endpoint with direct API access in Python. The results showed cross-AZ latency is ~2.5x higher than same-AZ. However, direct API access requires manual HTTP request construction, auth token handling, and cache implementation.

This post implements the same pattern using Powertools for AWS Lambda (TypeScript) getMetadata(), verifies its caching behavior, and compares results with the Python approach.

Prerequisites:

  • Same test environment as Part 1 (ElastiCache Valkey 3AZ, VPC Lambda)
  • Node.js 22.x runtime
  • @aws-lambda-powertools/commons package

Powertools getMetadata()

Import getMetadata() from @aws-lambda-powertools/commons/utils/metadata and you're done:

TypeScript
import { getMetadata } from '@aws-lambda-powertools/commons/utils/metadata';
 
const metadata = await getMetadata();
const azId = metadata.AvailabilityZoneID; // e.g., "apne1-az2"

Compare with direct API access:

Direct API access (for comparison)
const api = process.env.AWS_LAMBDA_METADATA_API!;
const token = process.env.AWS_LAMBDA_METADATA_TOKEN!;
const res = await fetch(
  `http://${api}/2026-01-15/metadata/execution-environment`,
  { headers: { Authorization: `Bearer ${token}` } }
);
const { AvailabilityZoneID: azId } = await res.json();

Environment variable lookup, URL construction, and auth header — all collapsed into one getMetadata() call.

Powertools Internals

Examining the source code:

AspectBehavior
CachingModule-level object stores the result. No HTTP request on subsequent calls
Cache clearclearMetadataCache() for explicit invalidation
Local devReturns empty object when POWERTOOLS_DEV=true or AWS_LAMBDA_INITIALIZATION_TYPE is unset
TimeoutDefault 1000ms (AbortSignal.timeout), configurable via options
HTTP clientNode.js built-in fetch API

Notable is clearMetadataCache(). In SnapStart environments, you need to invalidate the cache after Restore to get the new AZ ID.

Caching Behavior Verification

Measured getMetadata() latency in a non-VPC Lambda:

TypeScript
// 1st: Initial call
const metadata1 = await getMetadata();
 
// 2nd: Cache hit
const metadata2 = await getMetadata();
 
// After clearMetadataCache(): Re-fetch
clearMetadataCache();
const metadata3 = await getMetadata();
MeasurementCold StartWarm Start
First call605ms0.05–0.19ms (cache from previous invocation)
Cache hit0.07ms0.015–0.024ms
After clearMetadataCache()20ms1.9–158ms

The cold start first call (~600ms) includes Lambda initialization overhead. On warm starts, the module-level cache persists across invocations, so even the "first call" completes in ~0.05ms.

The post-clearMetadataCache() variance (1.9–158ms) reflects the cost of establishing a new HTTP connection each time. Avoid calling clearMetadataCache() in normal code paths — use it only when cache invalidation is genuinely needed, such as after SnapStart Restore.

Same-AZ Routing Verification

Tested under the same conditions as Part 1 (ElastiCache Valkey 3AZ, TLS, 50 PING iterations).

Lambda Function

The handler is straightforward: getMetadata() for the AZ ID, then measure latency to each node and classify by same-AZ vs cross-AZ.

index.ts (handler)
import { getMetadata } from '@aws-lambda-powertools/commons/utils/metadata';
 
export const handler = async () => {
  const metadata = await getMetadata();
  const lambdaAzId = (metadata as Record<string, unknown>).AvailabilityZoneID as string;
 
  const nodes = process.env.CACHE_NODES!.split(',').map(e => {
    const [id, address, port, azId, azName, role] = e.split('|');
    return { id, address, port: parseInt(port), azId, azName, role };
  });
 
  const results = await Promise.all(nodes.map(async node => ({
    ...node,
    latency: await measureValkeyLatency(node.address, node.port),
    sameAz: node.azId === lambdaAzId,
  })));
 
  return { lambdaAzId, results };
};

AZ ID retrieval is a single getMetadata() call. The entire function bundles to just 5.3KB with esbuild.

Full Lambda function code (including latency measurement)
index.ts
import { getMetadata } from '@aws-lambda-powertools/commons/utils/metadata';
import * as tls from 'tls';
 
function measureValkeyLatency(host: string, port: number, iterations = 50): Promise<{avgMs: number}> {
  return new Promise((resolve, reject) => {
    const socket = tls.connect({ host, port, servername: host }, () => {
      const latencies: number[] = [];
      let i = 0;
      const ping = () => {
        if (i >= iterations) {
          socket.destroy();
          latencies.sort((a, b) => a - b);
          resolve({
            avgMs: Math.round((latencies.reduce((a, b) => a + b, 0) / latencies.length) * 1000) / 1000,
          });
          return;
        }
        const start = performance.now();
        socket.write('*1\r\n$4\r\nPING\r\n');
        socket.once('data', () => { latencies.push(performance.now() - start); i++; ping(); });
      };
      ping();
    });
    socket.on('error', reject);
  });
}
 
export const handler = async () => {
  const metadata = await getMetadata();
  const lambdaAzId = (metadata as Record<string, unknown>).AvailabilityZoneID as string;
 
  const nodes = process.env.CACHE_NODES!.split(',').map(e => {
    const [id, address, port, azId, azName, role] = e.split('|');
    return { id, address, port: parseInt(port), azId, azName, role };
  });
 
  const results = await Promise.all(nodes.map(async node => ({
    ...node,
    latency: await measureValkeyLatency(node.address, node.port),
    sameAz: node.azId === lambdaAzId,
  })));
 
  return { lambdaAzId, results };
};
Deployment steps

Build the ElastiCache Valkey cluster using the deployment steps from Part 1. For the TypeScript Lambda:

Terminal (build)
npm init -y
npm install @aws-lambda-powertools/commons esbuild
npx esbuild index.ts --bundle --platform=node --target=node22 --outfile=dist/index.js --format=cjs --minify
cd dist && zip function.zip index.js
Terminal (Lambda creation)
aws lambda create-function \
  --function-name az-ts-routing-test \
  --runtime nodejs22.x \
  --handler index.handler \
  --role arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME> \
  --zip-file fileb://function.zip \
  --timeout 120 --memory-size 512 \
  --vpc-config SubnetIds=<SUBNET_1a>,<SUBNET_1c>,<SUBNET_1d>,SecurityGroupIds=<SG_ID> \
  --environment '{"Variables":{"CACHE_NODES":"<CACHE_NODES_VALUE>"}}'

See Part 1 for the CACHE_NODES format.

Results

16 invocations across all 3 AZs:

MetricSame-AZCross-AZ
Average0.739 ms1.894 ms
Minimum0.428 ms1.579 ms
Maximum1.634 ms2.141 ms

Same-AZ routing reduces average latency by ~61% (1.894ms to 0.739ms). Cross-AZ is roughly 2.6x slower.

Full measurement data (all 16 runs)
RunLambda AZSame-AZ avg (ms)Cross-AZ avg (ms)Overhead
1apne1-az40.4281.855+333%
2apne1-az40.5811.926+232%
3apne1-az40.5391.883+249%
4apne1-az41.6342.130+30%
5apne1-az40.6972.141+207%
6apne1-az40.7522.014+168%
7apne1-az20.5381.707+217%
8apne1-az40.5771.905+230%
9apne1-az40.6522.055+215%
10apne1-az40.6072.036+235%
11apne1-az20.6791.579+133%
12apne1-az40.7492.014+169%
13apne1-az20.5841.637+180%
14apne1-az11.1311.929+71%
15apne1-az40.5251.889+260%
16apne1-az11.1581.599+38%

Comparison with Python (Part 1)

MetricPython (Direct API)TypeScript (Powertools)
Same-AZ avg0.663 ms0.739 ms
Cross-AZ avg1.663 ms1.894 ms
Ratio2.5x2.6x
Reduction~60%~61%

The same-AZ vs cross-AZ ratio is virtually identical. The absolute difference (TypeScript slightly slower) likely reflects runtime differences and ElastiCache cluster re-provisioning. The key takeaway: Powertools adds no measurable overhead to routing effectiveness.

Takeaways

  • TypeScript Powertools works now@aws-lambda-powertools/commons 2.32.0 includes getMetadata() with automatic caching.
  • Same-AZ routing benefit is runtime-agnostic — Both Python direct API and TypeScript Powertools show ~2.5x cross-AZ overhead consistently.
  • clearMetadataCache() is not for normal code paths — Post-clear re-fetch takes 1.9–158ms with high variance. Reserve for SnapStart Restore or similar scenarios.

Cleanup

Same procedure as Part 1, with the TypeScript Lambda function names:

Terminal
aws lambda delete-function --function-name az-ts-routing-test
aws lambda delete-function --function-name az-ts-powertools-test
# For ElastiCache, subnet group, SG, and IAM role cleanup, see Part 1:

Share this post

Shinya Tahara

Shinya Tahara

Solutions Architect @ AWS

I'm a Solutions Architect at AWS, providing technical guidance primarily to financial industry customers. I share learnings about cloud architecture and AI/ML on this site.The views and opinions expressed on this site are my own and do not represent the official positions of my employer.

Related Posts