Skip to content

Lambda functions should be configured with a dead-letter queue

When a Lambda function exhausts retries on an asynchronous invocation, the event payload disappears unless a dead-letter queue captures it. That means data loss with no notification, no forensic trail, and no path to remediation. In regulated environments, silent failure is not acceptable.

A DLQ backed by SQS or SNS gives your operations team a concrete signal when processing breaks. You can wire alerts, reprocess failed events, and keep an audit trail of failures. An SQS queue sitting idle costs virtually nothing, so there is no good reason to skip this.

Retrofit consideration

Retrofitting means auditing each function to identify an appropriate SQS or SNS target, then updating its IAM execution role with sqs:SendMessage or sns:Publish on that ARN. Also verify that downstream consumers can handle replayed events before the DLQ goes live. If a queue has never received traffic before, a sudden replay can surprise unprepared consumers.

Implementation

Choose the approach that matches how you manage Terraform.

Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.

module "lambda" {
  source  = "soc2.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "hipaa.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "nist80053.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "fedrampmoderate.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "nist800171.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "ffiec.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "rbiitfnbfc.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "fedramplow.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "hipaasecurity2003.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

module "lambda" {
  source  = "nistcsfv11.compliance.tf/terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

If you use terraform-aws-modules/lambda/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.

module "lambda" {
  source  = "terraform-aws-modules/lambda/aws"
  version = ">=8.0.0"

  create_package         = false
  function_name          = "abc123"
  handler                = "index.lambda_handler"
  local_existing_package = "lambda_function.zip"
  runtime                = "python3.12"

  dead_letter_target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
}

Use AWS provider resources directly. See docs for the resources involved: aws_lambda_function.

resource "aws_lambda_function" "this" {
  filename                       = "lambda_function.zip"
  function_name                  = "pofix-abc123"
  handler                        = "index.handler"
  reserved_concurrent_executions = 100
  role                           = "arn:aws:iam::123456789012:role/example-role"
  runtime                        = "python3.12"
  source_code_hash               = "base64encodedhashabcdef1234567890=="

  dead_letter_config {
    target_arn = "arn:aws:sqs:us-east-1:123456789012:example-queue"
  }
}

What this control checks

This control validates that every aws_lambda_function resource has a dead_letter_config block with a non-empty target_arn pointing to an SQS queue or SNS topic ARN. It fails when the block is absent or target_arn is empty. The execution role also needs sqs:SendMessage or sns:Publish on the target ARN, though this check only evaluates the dead_letter_config.target_arn value, not the IAM policy.

One important distinction: aws_lambda_function_event_invoke_config with destination_config.on_failure configures Lambda Destinations, a separate mechanism. Only the dead_letter_config block on the aws_lambda_function resource satisfies this control.

Common pitfalls

  • Missing IAM permissions on execution role

    DLQ delivery silently fails if the Lambda execution role lacks sqs:SendMessage or sns:Publish on the target ARN. Lambda records these as DeadLetterErrors in CloudWatch but does not surface them as invocation failures, so the function appears to run normally while events are being lost. Always attach the required policy to the role specified in the function's role argument.

  • Confusing Lambda Destinations with dead_letter_config

    Lambda Destinations and dead_letter_config are not interchangeable. aws_lambda_function_event_invoke_config with destination_config.on_failure is a different mechanism from the dead_letter_config block on aws_lambda_function. This control checks only the latter. A function with only a Destination configured will fail the check.

  • DLQ target in a different account or region

    Lambda DLQ targets must be in the same region as the function. Cross-account targets work when both the target's resource policy and the execution role's IAM permissions allow access, but if the target_arn references something the function cannot reach, the DLQ exists in configuration and nowhere else. Verify reachability before treating the config as evidence of capture.

  • SQS queue message retention too short

    The default SQS MessageRetentionPeriod is 4 days. If nobody monitors or drains the DLQ within that window, failed events are permanently lost regardless of the DLQ being configured. Set message_retention_seconds on aws_sqs_queue to a value that matches your incident response SLA, up to the maximum of 1209600 seconds (14 days).

Audit evidence

Config rule evaluation results showing all Lambda functions as compliant, with each having a dead-letter queue target configured. Supporting evidence includes Console screenshots of individual Lambda function configurations with the DLQ section populated, or aws lambda get-function-configuration output showing a non-empty DeadLetterConfig.TargetArn for each function.

CloudWatch DeadLetterErrors metrics for monitored functions show whether DLQ delivery is actually working. Including these metrics rounds out the evidence picture by demonstrating the DLQ configuration is operational, not just present.

Framework-specific interpretation

SOC 2: Processing Integrity and Availability both apply here. Processing Integrity asks whether processing is complete and valid; a silently dropped invocation is a direct failure of that criterion. The DLQ is what gives auditors evidence that incomplete processing events are captured rather than lost.

HIPAA Omnibus Rule 2013: Lambda functions processing ePHI that fail silently create exactly the kind of incident detection gap that the HIPAA Security Rule's administrative safeguards are designed to close. 45 CFR 164.308(a)(6) requires covered entities to identify and respond to security incidents. A DLQ captures processing failures involving ePHI and gives the security team something concrete to investigate, rather than a gap in the audit trail.

NIST SP 800-53 Rev 5: SI-11 says error conditions should be visible only to authorized personnel, not silently discarded. IR-5 requires incident monitoring. Routing Lambda failures to a controlled SQS queue or SNS topic covers both: failures are retained for authorized review and feed directly into the incident monitoring pipeline.

FedRAMP Moderate Baseline Rev 4: SI-11 and IR-6 are the relevant controls. SI-11 requires that systems handle error conditions without discarding data or exposing sensitive information to unauthorized parties; IR-6 requires incident reporting mechanisms. A DLQ satisfies both: failures are captured rather than dropped, and the SQS queue or SNS topic gives the incident response pipeline a concrete notification path.

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: lambda_function_dead_letter_queue_configured

  • AWS Config Managed Rule: LAMBDA_DLQ_CHECK

  • Checkov Check: CKV_AWS_116

  • Powerpipe Control: aws_compliance.control.lambda_function_dead_letter_queue_configured

  • KICS Query: 720f44cf-285e-4b69-8f72-835e6bc1dceb

Last reviewed: 2026-03-09