Skip to content

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

FixDevs ·

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

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

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"
}

After changing IaC config, always redeploy:

# CDK
cdk deploy

# SAM
sam deploy

# Terraform
terraform apply

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"
    }
  ]
}

Lambda Powertools Parameters utility (recommended 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: 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

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 6: 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.

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.

For related AWS issues, see Fix: AWS IAM Permission Denied and Fix: AWS CloudWatch Logs Not Appearing.

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