Fixing AWS S3 Event Notifications That Silently Stop Triggering Lambdas

May 28, 2026 6 min read 57 views
Minimalist illustration of a cloud storage bucket linked to a serverless function icon with a broken connection line on a blue-grey background

You upload a file to S3, your application confirms the PUT succeeded, and your Lambda is supposed to kick off a processing pipeline. But nothing happens. No CloudWatch logs, no errors, no DLQ messages. The bucket looks fine, the Lambda looks fine, and yet the work never runs.

Silent failures in S3 event notifications are surprisingly common. The problem is they leave almost no evidence by default, so you end up questioning your entire setup when the actual issue is usually one small misconfiguration.

  • How S3 event notification delivery actually works under the hood
  • The most common reasons notifications stop firing without any error
  • How to verify permissions, filters, and resource policies step by step
  • How to test end-to-end without waiting for real uploads
  • Patterns to make future failures visible instead of silent

How S3 Event Notifications Work (and Where They Can Break)

When S3 fires an event notification, it acts as the caller, not the other way around. S3 makes a synchronous call to your Lambda on your behalf, using its own internal service principal. Your Lambda doesn't poll S3 β€” S3 pushes to Lambda.

This matters because the permission model is inverted from what most developers expect. Your Lambda execution role doesn't need permission to read from S3 to receive an event. Instead, your Lambda's resource-based policy needs to explicitly allow lambda:InvokeFunction from the S3 service principal for that specific bucket.

The delivery path looks like this: S3 event occurs β†’ S3 reads the notification configuration on the bucket β†’ S3 calls lambda:InvokeFunction on the target β†’ Lambda executes. A misconfiguration at any of those steps kills the whole chain, silently.

Start With the Lambda Resource Policy

This is the most commonly broken piece. Check it first.

Run this command against your Lambda function:

aws lambda get-policy \
  --function-name your-function-name \
  --region us-east-1

You're looking for a statement that grants lambda:InvokeFunction to the principal s3.amazonaws.com, with a SourceArn matching your bucket's ARN. A valid statement looks like this:

{
  "Sid": "AllowS3Invoke",
  "Effect": "Allow",
  "Principal": {
    "Service": "s3.amazonaws.com"
  },
  "Action": "lambda:InvokeFunction",
  "Resource": "arn:aws:lambda:us-east-1:123456789012:function:your-function-name",
  "Condition": {
    "ArnLike": {
      "AWS:SourceArn": "arn:aws:s3:::your-bucket-name"
    }
  }
}

If this statement is missing, S3 cannot invoke your Lambda at all. Add it with:

aws lambda add-permission \
  --function-name your-function-name \
  --statement-id AllowS3Invoke \
  --action lambda:InvokeFunction \
  --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::your-bucket-name \
  --region us-east-1

Note the SourceArn uses arn:aws:s3::: with no account ID or region. That's intentional β€” S3 bucket ARNs are global.

Verify the Bucket Notification Configuration

Even if the Lambda permission exists, the bucket itself might not be pointing at the right target.

aws s3api get-bucket-notification-configuration \
  --bucket your-bucket-name

Check the output for a LambdaFunctionConfigurations block. Verify the LambdaFunctionArn points to the correct function ARN, including the correct region and account ID. A stale ARN left over from a previous deployment is a very common cause of silent failures after a function rename or account migration.

Also check the Events list. If you're expecting a trigger on s3:ObjectCreated:Put but the config only lists s3:ObjectCreated:CompleteMultipartUpload, your direct PUT uploads will never fire.

Understand How Prefix and Suffix Filters Work

S3 notification filters look simple but have a real gotcha: you can only have one notification configuration per event type per prefix/suffix combination. Overlapping filter rules silently discard conflicting configurations when you save them through the console.

Consider this scenario: you add a rule for s3:ObjectCreated:* on prefix input/, and later add another rule for the same event on the same prefix pointing to a different Lambda. S3 accepts the second rule but quietly drops the first. No error, no warning.

Always use the API to inspect the full notification config rather than trusting the console view:

aws s3api get-bucket-notification-configuration \
  --bucket your-bucket-name \
  --output json | python3 -m json.tool

Look for duplicate or overlapping filter blocks and consolidate them. If you need to fan out to multiple Lambdas, use SNS or EventBridge as an intermediary instead of stacking direct S3 notification rules.

Check for Cross-Account and Cross-Region Issues

If your S3 bucket and your Lambda are in different AWS accounts or different regions, the default setup will not work and will fail silently.

S3 event notifications do not support cross-region Lambda invocation. If your bucket is in us-east-1 and your Lambda is in eu-west-1, notifications will never fire. The bucket and function must be in the same region.

Cross-account delivery is technically possible but requires both the resource-based policy on the Lambda and a bucket policy permitting the cross-account S3 service principal. In practice, it's easier to route through EventBridge, which handles cross-account fan-out cleanly. If you suspect cross-account drift, verify the account ID in the Lambda ARN stored in the notification config matches the account that owns the function.

Use EventBridge as a More Observable Alternative

AWS S3 can publish events to EventBridge instead of calling Lambda directly. This gives you better visibility, retry behavior, and the ability to add multiple targets without the filter overlap problem.

Enable EventBridge notifications on the bucket:

aws s3api put-bucket-notification-configuration \
  --bucket your-bucket-name \
  --notification-configuration '{"EventBridgeConfiguration": {}}'

Then create an EventBridge rule that matches S3 events and routes to your Lambda. The key advantage is that EventBridge has its own event history and dead-letter queue support, so failures surface in CloudWatch instead of disappearing. You also get structured filtering on object keys, sizes, and custom metadata without the overlap constraints of native S3 filters.

Test Without Uploading Real Files

Waiting for actual S3 uploads to debug notification config is slow and frustrating. You can invoke your Lambda directly with a synthetic S3 event to verify the function itself works:

aws lambda invoke \
  --function-name your-function-name \
  --payload file://test-s3-event.json \
  --cli-binary-format raw-in-base64-out \
  response.json

A minimal test payload (test-s3-event.json) looks like this:

{
  "Records": [
    {
      "eventVersion": "2.1",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-1",
      "eventTime": "2024-01-15T12:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "s3": {
        "bucket": {
          "name": "your-bucket-name",
          "arn": "arn:aws:s3:::your-bucket-name"
        },
        "object": {
          "key": "input/test-file.csv",
          "size": 1024
        }
      }
    }
  ]
}

If the direct invocation works but S3 still doesn't trigger the function, the problem is in the notification config or permissions layer, not your function code.

To verify S3 can actually reach the Lambda, upload a small test file using the AWS CLI and immediately tail CloudWatch logs:

aws s3 cp test.txt s3://your-bucket-name/input/test.txt
aws logs tail /aws/lambda/your-function-name --follow

Common Pitfalls That Catch People Out

Lambda version or alias drift. If your notification config points to a specific Lambda version ARN (e.g., :7) rather than the unqualified function ARN or an alias, deploying a new version won't update the trigger. The event keeps going to the old version. Use an alias like :live or the unqualified ARN to avoid this.

Terraform or CDK destroying and recreating the resource policy. Infrastructure-as-code tools that manage the Lambda permission as a separate resource will sometimes delete and recreate it during a plan that touches the function. During the brief window between deletion and recreation, events are lost silently. Adding an explicit dependency between the notification config and the permission resource fixes this.

Bucket versioning and notification event types. On a versioning-enabled bucket, s3:ObjectCreated:Put fires for all versions. But if you're filtering by prefix and a delete marker is created, that fires a different event type (s3:ObjectRemoved) that you may not have subscribed to.

The 100-notification limit. A single S3 bucket can have at most 100 notification configurations. It's unlikely you'll hit this in most setups, but if you're programmatically adding rules and forgetting to clean up old ones, you'll eventually hit the limit and new configurations will fail to save without a clear error.

Make Future Failures Visible

The real fix isn't just resolving the current incident β€” it's making the next failure detectable before a user reports it.

Add a CloudWatch alarm on the Lambda's Invocations metric. If your pipeline normally processes uploads around the clock and the invocation count drops to zero for a sustained period, that's a signal worth alerting on. Pair it with a metric filter on Errors to separate

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.