Skip to content

Fix: AWS Lambda Environment Variable Not Set — undefined or Missing at Runtime

FixDevs · (Updated: )

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 environments

Or 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 value

Or 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:GetParameter permission, 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 $LATEST don’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:Decrypt permission on the encryption key.
  • Lambda@Edge restrictions — Lambda@Edge functions do not support environment variables at all. Any process.env access returns undefined regardless 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 decryption

After changing IaC config, always redeploy:

# CDK
cdk deploy

# SAM
sam deploy

# Terraform
terraform apply

# Serverless Framework
sls deploy

Fix 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 works

If 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 key

Verify 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 script

CloudFront 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.json

Important 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 libraries

Size 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=prod

Still 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 $VERSION

VPC 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 Interface

Container 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 valuesStringParameter.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles