Fix: AWS Lambda Environment Variable Not Set — undefined or Missing at Runtime
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix AWS Lambda environment variables not available — Lambda console config, CDK/SAM/Terraform setup, secrets from SSM Parameter Store, encrypted variables, and local testing.
The Problem
An environment variable is undefined inside a Lambda function despite being configured:
exports.handler = async (event) => {
const dbUrl = process.env.DATABASE_URL;
console.log('DB URL:', dbUrl); // Prints: DB URL: undefined
// Function fails because DATABASE_URL is not set
};Or variables are set in one environment (staging) but missing in another (production):
Error: DATABASE_URL is required but was not provided
// Works in staging Lambda, fails in production Lambda
// Config drift between environmentsOr a variable is set but contains the wrong value — the SSM parameter path instead of its value:
process.env.API_KEY // "arn:aws:ssm:us-east-1:123:parameter/prod/api-key"
// Returns the ARN, not the actual secret valueOr environment variables set via CDK or Terraform aren’t reflecting in the deployed function.
Why This Happens
Lambda environment variables must be explicitly set on each function. Unlike EC2 instances, Lambda doesn’t inherit environment variables from the host or deployment pipeline. Each function version and alias carries its own isolated set of variables, and no mechanism propagates them across functions or accounts automatically.
The behavior also differs depending on which tool you use to define them. The Lambda console, SAM, CDK, Terraform, and the Serverless Framework each have their own syntax and deployment lifecycle. A variable defined in a SAM template.yaml only takes effect after sam deploy completes, not when the file is saved. CDK resolves SSM parameters at synth time by default, while SAM’s {{resolve:ssm:...}} syntax resolves at CloudFormation deploy time. These timing differences are a frequent source of “I set it, why isn’t it there?”
Common failure patterns:
- Variables set in the wrong Lambda function — AWS has no global Lambda environment. Each function version/alias has its own isolated environment variables.
- IaC not deployed — CDK, SAM, or Terraform config defines the variable, but the stack wasn’t redeployed after adding the new variable.
- SSM/Secrets Manager reference not resolved — some tools support referencing SSM parameters by ARN. If the Lambda execution role lacks
ssm:GetParameterpermission, or the reference syntax is wrong, the raw ARN ends up in the variable instead of the value. - Lambda version/alias pointing to old code — if an alias points to a published version, environment variable changes on
$LATESTdon’t affect the alias until a new version is published. - Encrypted variables not decrypted — KMS-encrypted environment variables require the Lambda execution role to have
kms:Decryptpermission on the encryption key. - Lambda@Edge restrictions — Lambda@Edge functions do not support environment variables at all. Any
process.envaccess returnsundefinedregardless of console configuration.
Fix 1: Verify Variables Are Set on the Correct Function
Check the environment variables actually configured on the specific Lambda function and alias:
# List environment variables on a Lambda function
aws lambda get-function-configuration \
--function-name my-function \
--query 'Environment.Variables'
# For a specific alias
aws lambda get-function-configuration \
--function-name my-function \
--qualifier production \
--query 'Environment.Variables'
# For a specific version
aws lambda get-function-configuration \
--function-name my-function \
--qualifier 5 \
--query 'Environment.Variables'Set environment variables via AWS CLI:
# Set (or update) environment variables
# WARNING: --environment replaces ALL existing variables
# Always include existing variables or they'll be removed
aws lambda update-function-configuration \
--function-name my-function \
--environment "Variables={DATABASE_URL=postgres://...,JWT_SECRET=mysecret,NODE_ENV=production}"
# To add a variable without removing others, first get existing vars:
EXISTING=$(aws lambda get-function-configuration \
--function-name my-function \
--query 'Environment.Variables' \
--output json)
# Then merge and update (example using jq)
aws lambda update-function-configuration \
--function-name my-function \
--environment "Variables=$(echo $EXISTING | jq '. + {"NEW_VAR": "new-value"}' -c)"Fix 2: Configure Variables in Infrastructure as Code
Each IaC tool has its own syntax and deployment behavior. Here’s how to set Lambda environment variables correctly in each.
AWS CDK:
// CDK — Lambda function with environment variables
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
const myFunction = new Function(this, 'MyFunction', {
runtime: Runtime.NODEJS_20_X,
code: Code.fromAsset('lambda'),
handler: 'index.handler',
// Direct environment variables
environment: {
NODE_ENV: 'production',
LOG_LEVEL: 'info',
// DON'T put secrets here — visible in CloudFormation template
// DATABASE_URL: 'postgres://...', // WRONG for secrets
},
});
// Read value from SSM at deploy time (baked into Lambda config)
const dbUrl = StringParameter.valueForStringParameter(
this, '/prod/database-url'
);
myFunction.addEnvironment('DATABASE_URL', dbUrl);
// Grant SSM access if reading at runtime instead
// myFunction.addToRolePolicy(new PolicyStatement({
// actions: ['ssm:GetParameter'],
// resources: ['arn:aws:ssm:*:*:parameter/prod/*'],
// }));SAM template:
# template.yaml
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: index.handler
Runtime: nodejs20.x
Environment:
Variables:
NODE_ENV: production
LOG_LEVEL: info
# Reference SSM parameter (resolved at deploy time)
DATABASE_URL: !Sub '{{resolve:ssm:/prod/database-url}}'
# Or SSM SecureString (KMS encrypted)
API_KEY: !Sub '{{resolve:ssm-secure:/prod/api-key}}'Terraform:
# main.tf
resource "aws_lambda_function" "my_function" {
filename = "function.zip"
function_name = "my-function"
role = aws_iam_role.lambda_role.arn
handler = "index.handler"
runtime = "nodejs20.x"
environment {
variables = {
NODE_ENV = "production"
LOG_LEVEL = "info"
# Reference SSM parameter
DATABASE_URL = data.aws_ssm_parameter.db_url.value
}
}
}
data "aws_ssm_parameter" "db_url" {
name = "/prod/database-url"
}Serverless Framework:
# serverless.yml
provider:
name: aws
runtime: nodejs20.x
environment:
# Global — shared by all functions
NODE_ENV: production
functions:
myFunction:
handler: index.handler
environment:
# Function-specific — overrides or adds to global
DATABASE_URL: ${ssm:/prod/database-url}
API_KEY: ${ssm:/prod/api-key~true} # ~true for SecureString decryptionAfter changing IaC config, always redeploy:
# CDK
cdk deploy
# SAM
sam deploy
# Terraform
terraform apply
# Serverless Framework
sls deployFix 3: Fetch Secrets at Runtime from SSM or Secrets Manager
For sensitive values, don’t bake them into Lambda environment variables — fetch at runtime:
// Using AWS SDK v3
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const ssm = new SSMClient({ region: process.env.AWS_REGION });
const secretsManager = new SecretsManagerClient({ region: process.env.AWS_REGION });
// Cache secrets outside the handler — reused across warm invocations
let cachedSecrets = null;
async function getSecrets() {
if (cachedSecrets) return cachedSecrets;
// Fetch from SSM Parameter Store
const [dbParam, apiKeyParam] = await Promise.all([
ssm.send(new GetParameterCommand({
Name: '/prod/database-url',
WithDecryption: true, // Required for SecureString parameters
})),
ssm.send(new GetParameterCommand({
Name: '/prod/api-key',
WithDecryption: true,
})),
]);
cachedSecrets = {
databaseUrl: dbParam.Parameter.Value,
apiKey: apiKeyParam.Parameter.Value,
};
return cachedSecrets;
}
export const handler = async (event) => {
const { databaseUrl, apiKey } = await getSecrets();
// Use secrets...
};IAM permissions for SSM access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": "arn:aws:ssm:us-east-1:123456789:parameter/prod/*"
},
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:us-east-1:123456789:key/your-key-id"
}
]
}AWS Parameters and Secrets Lambda Extension (recommended for production):
The extension runs as a Lambda layer and caches SSM/Secrets Manager values locally inside the execution environment. It exposes a local HTTP endpoint that your function queries instead of calling the SSM API directly, reducing latency and API throttling risk:
# Add the extension layer to your function
aws lambda update-function-configuration \
--function-name my-function \
--layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11// Fetch from the local extension endpoint — no SDK import needed
const http = require('http');
async function getParameter(name) {
const url = `http://localhost:2773/systemsmanager/parameters/get?name=${encodeURIComponent(name)}&withDecryption=true`;
const response = await fetch(url, {
headers: { 'X-Aws-Parameters-Secrets-Token': process.env.AWS_SESSION_TOKEN },
});
const data = await response.json();
return data.Parameter.Value;
}Lambda Powertools Parameters utility (for Python/TypeScript):
// @aws-lambda-powertools/parameters — caches and handles refresh automatically
import { SSMProvider } from '@aws-lambda-powertools/parameters/ssm';
const ssm = new SSMProvider();
export const handler = async () => {
// Automatically cached for 5 minutes (configurable)
const dbUrl = await ssm.get('/prod/database-url', { decrypt: true });
// Get multiple parameters at once
const params = await ssm.getMultiple('/prod/', {
decrypt: true,
recursive: true,
});
// Returns: { 'database-url': '...', 'api-key': '...' }
};Fix 4: Handle Encrypted Environment Variables
Lambda supports KMS-encrypted environment variables. Decryption requires the execution role to have kms:Decrypt permission:
# Set a KMS-encrypted environment variable
aws lambda update-function-configuration \
--function-name my-function \
--kms-key-arn arn:aws:kms:us-east-1:123:key/your-key-id \
--environment "Variables={SECRET_API_KEY=actual-secret-value}"
# Lambda encrypts the value with the KMS key at rest
# Lambda automatically decrypts on startup — process.env.SECRET_API_KEY worksIf decryption fails silently — check the Lambda execution role:
# Check what KMS permissions the Lambda role has
aws iam list-role-policies --role-name my-lambda-role
aws iam get-role-policy --role-name my-lambda-role --policy-name MyPolicy
# The role needs kms:Decrypt on the encryption keyVerify encryption is configured:
# Check if a KMS key is configured for the function
aws lambda get-function-configuration \
--function-name my-function \
--query 'KMSKeyArn'Fix 5: Handle Lambda@Edge and CloudFront Functions
Lambda@Edge functions do not support environment variables. This catches many developers off guard when migrating a standard Lambda function to the edge. Any process.env.MY_VAR access returns undefined regardless of what you configure in the console or IaC.
Workarounds for Lambda@Edge:
// Option 1: Hardcode values (acceptable for non-sensitive config)
const CONFIG = {
LOG_LEVEL: 'info',
FEATURE_FLAG: 'true',
};
// Option 2: Fetch from SSM/DynamoDB on first invocation and cache
let config = null;
exports.handler = async (event) => {
if (!config) {
// First invocation — fetch from a central config store
// Note: Lambda@Edge has network access to AWS services in the function's region
const ssm = new SSMClient({ region: 'us-east-1' });
const result = await ssm.send(new GetParameterCommand({
Name: '/edge/config',
WithDecryption: true,
}));
config = JSON.parse(result.Parameter.Value);
}
// Use config...
};
// Option 3: Bundle config at build time
// Build step injects values into the code before deployment
// webpack DefinePlugin, esbuild define, or a simple string replace scriptCloudFront Functions (lightweight alternative) also lack environment variable support. They run in a more restricted JavaScript runtime (not full Node.js). Use CloudFront KeyValueStore for configuration:
// CloudFront Function with KeyValueStore
import cf from 'cloudfront';
const kvsHandle = cf.kvs();
async function handler(event) {
const apiUrl = await kvsHandle.get('API_URL');
// ...
}Fix 6: Debug Missing Variables Locally
Test Lambda locally with SAM or serverless framework to catch missing variables before deployment:
# SAM local invoke with environment file
cat > env.json << 'EOF'
{
"MyFunction": {
"DATABASE_URL": "postgres://localhost:5432/mydb",
"API_KEY": "test-key",
"NODE_ENV": "development"
}
}
EOF
sam local invoke MyFunction --env-vars env.json --event event.jsonImportant SAM local vs deployed difference: sam local invoke does not resolve {{resolve:ssm:...}} dynamic references. The variable will contain the literal string {{resolve:ssm:/prod/database-url}} instead of the parameter value. Use the --env-vars file to provide local overrides, or set a fallback in your code:
const dbUrl = process.env.DATABASE_URL?.startsWith('{{resolve:')
? 'postgresql://localhost:5432/localdb' // SAM local fallback
: process.env.DATABASE_URL;Add runtime validation for required variables:
// index.js — validate required environment variables at startup
const REQUIRED_ENV_VARS = [
'DATABASE_URL',
'JWT_SECRET',
'API_KEY',
];
function validateEnvironment() {
const missing = REQUIRED_ENV_VARS.filter(key => !process.env[key]);
if (missing.length > 0) {
const error = `Missing required environment variables: ${missing.join(', ')}`;
console.error(error);
// Throw at startup — prevents Lambda from serving traffic with missing config
throw new Error(error);
}
}
// Run validation outside the handler — runs on cold start
validateEnvironment();
export const handler = async (event) => {
// By this point, all required vars are guaranteed to exist
const dbUrl = process.env.DATABASE_URL; // Safe to use
};Fix 7: Environment Variable Best Practices for Lambda
// Non-sensitive config — put directly in Lambda environment variables
// DATABASE_HOST, LOG_LEVEL, FEATURE_FLAGS, REGION, etc.
// Sensitive secrets — fetch from SSM/Secrets Manager at runtime
// DATABASE_PASSWORD, API_KEYS, JWT_SECRETS, etc.
// Lambda-provided variables — always available, no configuration needed
process.env.AWS_REGION // Region where function runs
process.env.AWS_LAMBDA_FUNCTION_NAME // Function name
process.env.AWS_LAMBDA_FUNCTION_VERSION // Current version
process.env.AWS_EXECUTION_ENV // Runtime identifier
process.env.LAMBDA_TASK_ROOT // Path to your function code
process.env.LAMBDA_RUNTIME_DIR // Path to runtime librariesSize limits — Lambda environment variables have a 4KB total size limit across all variables. Large configurations should be stored in S3 or SSM and fetched at runtime.
Per-environment configuration pattern with IaC:
// CDK — use context or parameter per environment
const stage = app.node.tryGetContext('stage') ?? 'dev';
const myFunction = new Function(this, 'MyFunction', {
environment: {
STAGE: stage,
TABLE_NAME: `orders-${stage}`,
// SSM paths differ per stage
DATABASE_URL: StringParameter.valueForStringParameter(
this, `/${stage}/database-url`
),
},
});
// Deploy: cdk deploy -c stage=prodStill Not Working?
Lambda alias pointing to old version — if you deploy using aliases (recommended), update the alias to point to the new version after deploying:
# Publish a new version
VERSION=$(aws lambda publish-version --function-name my-function --query Version --output text)
# Update alias to point to new version
aws lambda update-alias \
--function-name my-function \
--name production \
--function-version $VERSIONVPC Lambda can’t reach SSM — Lambda functions inside a VPC can’t reach SSM without a VPC endpoint or NAT gateway. Add an SSM VPC endpoint:
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345 \
--service-name com.amazonaws.us-east-1.ssm \
--vpc-endpoint-type InterfaceContainer image Lambda — for Lambda functions deployed as container images, environment variables set in the Dockerfile (ENV) are overridden by Lambda configuration. Lambda-set environment variables always take precedence.
Terraform ignore_changes silently blocking updates — if your Terraform resource uses lifecycle { ignore_changes = [environment] }, changes to environment variables in your .tf files will never be applied. Terraform plans will show no diff. Remove the ignore rule or manually update via the console.
CDK caching SSM values — StringParameter.valueForStringParameter() resolves the SSM value at synth time and bakes it into the CloudFormation template. If the SSM parameter changes after the last cdk deploy, the Lambda still uses the old value. Re-run cdk deploy to pick up the new value, or switch to runtime fetching for values that change frequently.
For related AWS issues, see Fix: AWS IAM Permission Denied, Fix: AWS CloudWatch Logs Not Appearing, Fix: AWS Lambda Timeout, and Fix: Terraform Variable Not Set.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: SST Not Working — Deploy Failing, Bindings Not Linking, or Lambda Functions Timing Out
How to fix SST (Serverless Stack) issues — resource configuration with sst.config.ts, linking resources to functions, local dev with sst dev, database and storage setup, and deployment troubleshooting.
Fix: AWS Lambda Layer Not Working — Module Not Found or Layer Not Applied
How to fix AWS Lambda Layer issues — directory structure, runtime compatibility, layer ARN configuration, dependency conflicts, size limits, and container image alternatives.
Fix: AWS Lambda SnapStart Not Working — Version vs Alias, Restore Hooks, and Uniqueness Bugs
How to fix Lambda SnapStart errors — feature requires published version, $LATEST not supported, restore hook for stale connections, UUID collisions after snapshot, time-based state staleness, and pricing surprises.
Fix: AWS Step Functions Not Working — ASL Syntax, Map State, Error Handling, and IAM
How to fix AWS Step Functions errors — Amazon States Language syntax, Standard vs Express workflows, Distributed Map for large datasets, Retry/Catch error handling, Lambda invoke optimization, and IAM execution role permissions.