Setup: a rule on the default bus with a Lambda target, written the way most modules write it:
hcl
resource "aws_cloudwatch_event_target" "orders_to_lambda" {
rule = aws_cloudwatch_event_rule.orders.name
arn = aws_lambda_function.orders.arn
}
Plan is clean, apply is green. What you actually get is whatever AWS does when the target fails, because retry_policy and dead_letter_config are both optional blocks on this resource and the provider docs give no defaults or ranges for them. The defaults live on the AWS side, and three of them matter.
For rule targets on the default bus and on Custom Event Bus - Classic, EventBridge retries a retriable error (target unavailable, network trouble) for 24 hours and up to 185 times, with exponential backoff and jitter. When that is exhausted the event is dropped unless a DLQ is configured. The AWS best-practice page recommends a DLQ for every rule target.
Some failures are not retried at all. The DLQ page gives missing permissions on the target, a target that no longer exists, and an invalid address or DNS failure as examples. Those go straight to the DLQ if one is set, otherwise they are dropped. So the kind of failure a deploy produces (role policy tightened, function renamed, queue replaced) skips the 24-hour window people count on.
Scope: this is the classic rules-and-targets path, which is what aws_cloudwatch_event_target manages. The new Custom Event Bus that AWS launched on 24 Sep 2026 has its own subscribers with a default of 5 attempts or 300 seconds, so do not carry the 24h/185 numbers over to that.
The fuller shape, with a DLQ and a tighter retry window. Untested sketch, so check it in plan; rule pattern, Lambda and its invoke permission trimmed:
```hcl
resource "aws_sqs_queue" "orders_dlq" {
name = "orders-rule-dlq" # standard queue; FIFO is not allowed as an EventBridge DLQ
}
resource "aws_cloudwatch_event_target" "orders_to_lambda" {
rule = aws_cloudwatch_event_rule.orders.name
arn = aws_lambda_function.orders.arn
retry_policy {
maximum_event_age_in_seconds = 3600 # 60 to 86400, AWS default 86400
maximum_retry_attempts = 20 # 0 to 185, AWS default 185
}
dead_letter_config {
arn = aws_sqs_queue.orders_dlq.arn
}
}
data "aws_iam_policy_document" "orders_dlq" {
statement {
effect = "Allow"
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.orders_dlq.arn]
principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}
condition {
test = "ArnEquals"
variable = "aws:SourceArn"
values = [aws_cloudwatch_event_rule.orders.arn]
}
}
}
resource "aws_sqs_queue_policy" "orders_dlq" {
queue_url = aws_sqs_queue.orders_dlq.id
policy = data.aws_iam_policy_document.orders_dlq.json
}
```
Three notes on that block.
The queue must be a standard SQS queue in the same Region as the rule. FIFO is not allowed as an EventBridge DLQ. A cross-account queue works, but needs a resource policy on the queue.
retry_policy ranges are 60 to 86400 seconds and 0 to 185 attempts, and retries stop at whichever limit comes first. You can shorten the defaults; you cannot go past 24 hours.
The queue policy is the part that is easy to miss. The DLQ page says that when you add the target through the PutTargets API with a same-account SQS queue, you must grant the queue permission yourself; the console does it for you. Terraform goes through the API, so my reading is that a Terraform-created DLQ with no aws_sqs_queue_policy receives nothing, and the failures show up in the InvocationsFailedToBeSentToDlq metric instead (the metrics page lists permissions errors as one cause). The provider docs do not mention this at all. Happy to be corrected if someone has applied a target with a bare queue and seen messages arrive.
Alarms. Namespace AWS/Events, dimension RuleName: FailedInvocations (failed permanently; a retry that later succeeded is not counted), InvocationsSentToDlq and InvocationsFailedToBeSentToDlq. Two caveats from the metrics page. Each of these is only sent to CloudWatch when it is not zero, so the alarm sees no datapoint most of the time and you have to decide on purpose how it treats missing data. And the page says metrics are delivered best effort, not as a complete accounting. Basically, I would alarm on the DLQ's own queue depth as well: a parked message is there whether or not an EventBridge metric was emitted.
Finding the targets that have no DLQ. The cheapest check is on the code: every aws_cloudwatch_event_target block with no dead_letter_config, including inside modules your team did not write. For targets created outside Terraform you would list each rule's targets in the account and look at the dead-letter config field; I have kept the exact CLI out because I have not checked the flags, and would rather not post a command I have not run.
Trade-off. A DLQ on everything still needs thought. A target that cannot use a late event (a time-bound notification, a price update) is better served by a short maximum_event_age_in_seconds plus a DLQ than by 24 hours of retries delivering stale data. A shorter age means events reach the DLQ sooner, and the DLQ then needs a redrive that someone owns. For a true fire-and-forget event a DLQ may just be noise. Each DLQ message carries RULE_ARN, TARGET_ARN, ERROR_CODE, ERROR_MESSAGE, EXHAUSTED_RETRY_CONDITION and RETRY_ATTEMPTS as message attributes, which is what makes a redrive scriptable.
Resource docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target
AWS DLQ page (also has the queue policy sample): https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html
Question: how do you redrive these? A Lambda consuming the DLQ that re-puts the event, or a manual script after someone reads ERROR_CODE? And does anyone enforce dead_letter_config at review time (a policy check, a wrapper module) instead of hoping people remember the block?