Guide · AWS AppConfig · Lambda
AWS AppConfig Lambda Extension for MCP Servers
The AppConfig Lambda Extension replaces SDK calls to AppConfig with a local HTTP GET on localhost:2772, reducing config retrieval from ~30ms to under 1ms on warm invocations. For MCP servers where every millisecond of tool latency matters, this is the preferred integration pattern over direct SDK polling. This guide covers adding the extension layer, the fetch API, and tuning the warm vs. cold-start tradeoffs. For the foundational setup of AppConfig resources, see AWS AppConfig for MCP Servers: Dynamic Configuration Without Redeployment. For feature-flag-specific patterns, see Feature Flags for MCP Servers with AWS AppConfig.
TL;DR
Add the AppConfig Lambda Extension as a layer ARN (region-specific). Set environment variables AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS=30 and the app/env/profile identifiers. Fetch config via http://localhost:2772/applications/{app}/environments/{env}/configurations/{profile} on each invocation. Grant your Lambda execution role appconfig:StartConfigurationSession and appconfig:GetLatestConfiguration. The extension runs as a separate process, handles session token rotation, and caches the config document in memory — your handler just does a plain HTTP GET with no SDK dependency.
What the Lambda Extension does
A Lambda Extension runs as a sibling process alongside your function handler within the same Lambda execution environment. The AppConfig extension starts when the execution environment initializes, opens a configuration session with AppConfig, and begins polling on a configurable interval. It stores the latest config document in memory and serves it over an HTTP server bound to localhost:2772.
Your handler code calls GET http://localhost:2772/... — this is a local loopback request that returns in under 1ms on a warm container, because the extension already has the cached document in memory. Compare this with a direct SDK call to AppConfig, which requires a TLS handshake and a round-trip to the AWS AppConfig regional endpoint — typically 25–40ms per call.
| Approach | Warm latency | Cold start overhead | Session management | SDK dependency in function |
|---|---|---|---|---|
| Lambda Extension (localhost) | <1ms | ~50ms (extension init) | Extension handles it | None — plain fetch() |
| Direct SDK polling (in-process) | 25–40ms per poll | None beyond SDK init | Manual token rotation | @aws-sdk/client-appconfigdata |
| SSM GetParameter on each invocation | 8–15ms | None | N/A | @aws-sdk/client-ssm |
The extension also handles token rotation automatically. The AppConfig GetLatestConfiguration API returns a new token on every call, and if you fail to use the latest token the session becomes invalid. Managing this in application code requires care — the extension removes that risk entirely.
Adding the extension ARN
The AppConfig Lambda Extension is published as a public Lambda layer. The ARN is region-specific and versioned. Always pin to a specific version in production rather than using :latest to avoid unexpected updates.
| Region | Layer ARN (x86_64, latest stable as of Oct 2026) |
|---|---|
| us-east-1 | arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:128 |
| us-west-2 | arn:aws:lambda:us-west-2:359756378197:layer:AWS-AppConfig-Extension:143 |
| eu-west-1 | arn:aws:lambda:eu-west-1:434848589818:layer:AWS-AppConfig-Extension:114 |
| ap-southeast-1 | arn:aws:lambda:ap-southeast-1:421114256042:layer:AWS-AppConfig-Extension:107 |
| ap-northeast-1 | arn:aws:lambda:ap-northeast-1:980059726660:layer:AWS-AppConfig-Extension:85 |
For ARM64 (Graviton2) functions, replace AWS-AppConfig-Extension with AWS-AppConfig-Extension-Arm64 in the ARN — the account IDs and version numbers differ per region, so check the AWS documentation for the current ARM64 ARN for your region.
CDK v2 example using addLayers():
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
const mcpHandler = new lambda.Function(this, 'McpServerHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
code: lambda.Code.fromAsset('dist'),
handler: 'index.handler',
timeout: cdk.Duration.seconds(30),
memorySize: 512,
environment: {
AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS: '30',
AWS_APPCONFIG_EXTENSION_HTTP_PORT: '2772',
APPCONFIG_APP: 'mcp-server',
APPCONFIG_ENV: 'production',
APPCONFIG_PROFILE: 'mcp-server-config',
},
layers: [
lambda.LayerVersion.fromLayerVersionArn(
this,
'AppConfigExtension',
// Pin to an exact version; do not use cdk.Fn.select tricks with "latest"
'arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:128',
),
],
});
// Grant the execution role permissions to read AppConfig
mcpHandler.addToRolePolicy(new iam.PolicyStatement({
actions: [
'appconfig:StartConfigurationSession',
'appconfig:GetLatestConfiguration',
],
resources: ['*'],
}));
In a SAM template, add the layer under the Layers property of your AWS::Serverless::Function resource. The same ARN applies; SAM does not require special syntax beyond listing it in the array.
Fetching config via the extension
The HTTP endpoint format is: http://localhost:2772/applications/{ApplicationIdentifier}/environments/{EnvironmentIdentifier}/configurations/{ConfigurationProfileIdentifier}. Identifiers can be the resource name (e.g., mcp-server) or the numeric ID returned by AppConfig APIs.
interface McpServerConfig {
toolEnabled: Record<string, boolean>;
rateLimits: Record<string, { requestsPerMinute: number; burstSize?: number }>;
upstreamTimeoutMs: Record<string, number>;
allowedCallers: string[];
logLevel: string;
maintenanceMode: boolean;
}
// Default config used before the extension populates its cache on cold start
const DEFAULT_CONFIG: McpServerConfig = {
toolEnabled: {},
rateLimits: {},
upstreamTimeoutMs: {},
allowedCallers: [],
logLevel: 'info',
maintenanceMode: false,
};
const APPCONFIG_PORT = process.env.AWS_APPCONFIG_EXTENSION_HTTP_PORT ?? '2772';
const APPCONFIG_APP = process.env.APPCONFIG_APP!;
const APPCONFIG_ENV = process.env.APPCONFIG_ENV!;
const APPCONFIG_PROFILE = process.env.APPCONFIG_PROFILE!;
const CONFIG_URL = `http://localhost:${APPCONFIG_PORT}/applications/${APPCONFIG_APP}/environments/${APPCONFIG_ENV}/configurations/${APPCONFIG_PROFILE}`;
let cachedConfig: McpServerConfig = DEFAULT_CONFIG;
let cachePopulated = false;
export async function refreshConfig(): Promise<McpServerConfig> {
const controller = new AbortController();
// 408 timeout means the extension hasn't completed its first poll yet —
// this is normal on the very first invocation after a cold start.
const timeoutId = setTimeout(() => controller.abort(), 500);
try {
const response = await fetch(CONFIG_URL, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
// 404 = profile not found; surface this immediately so the error is obvious
throw new Error(`AppConfig extension returned HTTP ${response.status}`);
}
const text = await response.text();
if (text.length > 0) {
cachedConfig = JSON.parse(text) as McpServerConfig;
cachePopulated = true;
}
return cachedConfig;
} catch (err: unknown) {
clearTimeout(timeoutId);
if (err instanceof Error && err.name === 'AbortError') {
// Extension hasn't populated yet; return the default on the first call,
// or the last good config on subsequent calls
console.warn('AppConfig extension not ready; using cached/default config');
return cachedConfig;
}
throw err;
}
}
// Call once at module load to prime the cache asynchronously.
// On warm invocations this returns immediately from the extension's memory.
const primePromise = refreshConfig();
export async function getMcpServerConfig(): Promise<McpServerConfig> {
await primePromise;
return cachedConfig;
}
The AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS environment variable controls how frequently the extension polls AppConfig. Minimum is 15 seconds. Values lower than 15 are clamped. For kill-switch use cases (disabling a broken tool fast), 15–30 seconds is appropriate. For lower-urgency configs like log level, 60–120 seconds reduces AppConfig API call volume and cost.
Optimizing for warm vs. cold starts
The extension polls AppConfig asynchronously during a cold start. This means your handler's first invocation after a cold start may read a stale or default config — the extension initializes in parallel with your handler code and may not have completed its first poll before your handler calls localhost:2772. This is intentional: it keeps cold start latency low.
If your MCP server absolutely must read the latest config before handling any request (for example, if the config contains allowed-caller lists that are a security boundary), set AWS_APPCONFIG_EXTENSION_POLL_WAIT_COMPLETE=true. This forces the extension to complete its first poll synchronously during initialization, before the handler invocation is allowed to start. The tradeoff is an additional ~30–80ms added to cold start time.
| Environment Variable | Default | Description |
|---|---|---|
AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS | 45 | How often the extension polls AppConfig for config changes. Minimum 15s. |
AWS_APPCONFIG_EXTENSION_HTTP_PORT | 2772 | Local port for the extension's HTTP server. Change only if 2772 conflicts with another extension. |
AWS_APPCONFIG_EXTENSION_POLL_WAIT_COMPLETE | false | If true, blocks handler start until the first AppConfig poll completes. Adds ~30–80ms to cold start. |
AWS_APPCONFIG_EXTENSION_LOG_LEVEL | info | Extension's own log level. Set to debug to troubleshoot poll failures in CloudWatch Logs. |
AWS_APPCONFIG_EXTENSION_MAX_CONNECTIONS | 3 | Max concurrent HTTP connections the extension opens to AppConfig. Increase if you have many profiles. |
For warm containers, the extension is already running and has a recent config cached. The HTTP GET to localhost:2772 completes in under 1ms — this is faster than any in-process cache lookup that involves JSON parsing, because the extension returns the raw bytes and your code only parses them when the document has actually changed. Use an ETag or content-hash comparison in your refreshConfig logic to avoid re-parsing an unchanged document on every invocation.
IAM permissions
The extension uses the Lambda function's execution role to authenticate with AppConfig. You need exactly two IAM actions. These cannot be scoped to a specific AppConfig Application ARN in all configurations — if you use resource names (rather than numeric IDs) in the URL, the IAM resource must be * unless you construct the exact ARN.
import * as iam from 'aws-cdk-lib/aws-iam';
// Minimum policy for the Lambda execution role
const appConfigPolicy = new iam.PolicyStatement({
sid: 'AppConfigReadConfig',
effect: iam.Effect.ALLOW,
actions: [
'appconfig:StartConfigurationSession',
'appconfig:GetLatestConfiguration',
],
// Scope to a specific application/environment/profile if you know the ARNs:
// resources: [
// `arn:aws:appconfig:us-east-1:${this.account}:application/${appId}`,
// `arn:aws:appconfig:us-east-1:${this.account}:application/${appId}/environment/${envId}`,
// `arn:aws:appconfig:us-east-1:${this.account}:application/${appId}/environment/${envId}/configuration/${profileId}`,
// ],
resources: ['*'],
});
mcpHandler.addToRolePolicy(appConfigPolicy);
Note: appconfig:GetConfiguration (the old API) and appconfig:GetLatestConfiguration are different IAM actions. The extension uses the newer AppConfigData API which requires StartConfigurationSession + GetLatestConfiguration. If you grant only appconfig:GetConfiguration you will get AccessDeniedException errors in the extension logs.
To verify the IAM setup before deploying, run the AWS CLI IAM policy simulator or check the extension's CloudWatch log group (/aws/lambda/{functionName}/AppConfig) for AccessDeniedException messages on the first invocation.
AliveMCP uptime monitoring + AppConfig Lambda Extension: complementary layers
The AppConfig Lambda Extension manages config delivery within your Lambda execution environment — it ensures your function has fresh configuration without the overhead of direct AppConfig SDK calls. AliveMCP works at the opposite end of the stack: it probes your MCP server from outside AWS every 60 seconds by sending a real JSON-RPC tools/list handshake and validating the response structure.
These two systems catch different failure modes. When a bad config deployment reaches your Lambda functions and causes them to crash on startup or reject requests, the extension cannot detect that — it only delivers configuration, it does not validate whether your application successfully consumed it. AliveMCP's external probe will detect within 60 seconds that tools/list is no longer returning a valid response. You can then immediately trigger a manual AppConfig rollback by calling StopDeployment or via the console — long before AppConfig's built-in bake timer would expire. The extension's asynchronous poll model means some Lambda containers may still be running the old (working) config while new ones pick up the bad config; AliveMCP sees the aggregate failure rate from a single probe point and alerts you without needing to instrument individual containers.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Extension returns HTTP 408 on first invocation after cold start | Extension hasn't completed its initial AppConfig poll; handler called localhost:2772 before the extension was ready |
Set AWS_APPCONFIG_EXTENSION_POLL_WAIT_COMPLETE=true to block handler start until first poll completes, or handle 408 by falling back to a safe default config |
| Warm containers serving stale config after a new AppConfig deployment | Extension poll interval hasn't elapsed on long-lived containers; config change deployed but old containers haven't re-polled | Reduce AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS to 15–30s for configs that need rapid propagation; accept that max propagation time equals the poll interval |
Connection refused on localhost:2772 |
Extension layer ARN not added to the function; the extension process is not running; wrong port in AWS_APPCONFIG_EXTENSION_HTTP_PORT |
Verify the layer ARN is listed in the function's layers; check CloudWatch Logs for extension startup messages; confirm port variable matches the fetch URL |
Error: spawn ENOENT or exit code 127 in extension log |
Layer ARN is from the wrong region; the extension binary is not present in the execution environment | Use the ARN for the same region as the Lambda function; ARM64 functions must use the ARM64 layer variant |
AccessDeniedException in extension CloudWatch logs on every poll |
Lambda execution role has appconfig:GetConfiguration but not appconfig:GetLatestConfiguration; the two are different IAM actions |
Add appconfig:StartConfigurationSession and appconfig:GetLatestConfiguration to the execution role; remove the deprecated appconfig:GetConfiguration action |