S3 Event Notification Not Triggering Lambda: "Unable to Validate the Following Destination Configurations," the 8 MB Multipart Trap, and URL-Encoded Keys, Fixed

Logeshwaran
—

When an S3 event notification doesn't trigger your Lambda function, the cause is usually one of five things: S3 has no permission to invoke the function (that's what "Unable to validate the following destination configurations" means), the event type doesn't match how the file was uploaded, a prefix or suffix filter never matches, someone replaced the bucket's notification configuration, or the function did run and crashed on the object key. The fix for the error is one command, aws lambda add-permission with --principal s3.amazonaws.com, then save the trigger again. The cause that catches the most people is quieter: a trigger set to s3:ObjectCreated:Put never fires for a file uploaded with aws s3 cp once it's 8 MB or bigger, because the CLI switches to a multipart upload, and that's a different event, s3:ObjectCreated:CompleteMultipartUpload. Choose s3:ObjectCreated:* and both are covered. This page shows how to tell which half is broken, each cause with the exact fix, and a handler that survives URL-encoded keys and test events.

Ethan, a developer, built a small photo pipeline for his friend Jake's phone repair shop: customers' photos of broken screens land in an S3 bucket, and a Lambda function makes a smaller copy for the repair ticket. It worked in every test. Then Jake started uploading photos from the shop's new phone with a script, and not one of them was processed.

⚡ Quick Answer

• "Unable to validate": S3 can't invoke the function. Add the permission. The fix.

• Big files never trigger: use s3:ObjectCreated:*, not :Put. Why.

• Just set it up? Changes can take about five minutes. Timing.

• Runs, then NoSuchKey: the key is URL-encoded. Decode it. The handler.

• Trigger vanished: a bucket has one notification configuration, and saving replaces it. Why.

Using a queue in between? Our SQS queue policy guide covers S3-to-SQS permissions.

New to these services? Amazon S3 stores files, called objects, in buckets, and AWS Lambda runs a piece of code when something happens. An S3 event notification is the setting on a bucket that connects the two: "when a file arrives, run this function."

🧭 NEW HERE? READ THESE FIRST

New to S3, Lambda or permissions? These five pages make the rest of this one easy:

📌 Bookmark this; the "which half is broken" table is the part you will come back for.

First, find out which half is broken

"The trigger doesn't work" covers two very different problems. Either S3 never invoked the function, or the function was invoked and failed. The function's Invocations metric in CloudWatch tells you which, in one look. This works in CloudShell or any Linux terminal:

aws cloudwatch get-metric-statistics --namespace AWS/Lambda \
  --metric-name Invocations --dimensions Name=FunctionName,Value=resize-photo \
  --start-time "$(date -u -d '-1 hour' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --period 3600 --statistics Sum

If the sum is zero after an upload, S3 never called the function, and the cause is in the bucket's notification settings or the permission. If it's above zero, the function ran, and its log has the answer:

aws logs tail /aws/lambda/resize-photo --since 15m
What you seeWhich halfMost likely cause
"Unable to validate the following destination configurations" when savingSetupS3 has no permission to invoke the function, or the function was deleted
Invocations = 0, small files work, big files don'tS3 sideEvent type is s3:ObjectCreated:Put; big uploads are multipart
Invocations = 0 for every fileS3 sideFilter never matches, trigger was replaced, wrong Region, or it's under five minutes old
Invocations > 0, NoSuchKey or AccessDenied in the logFunctionThe object key is URL-encoded and wasn't decoded
Invocations > 0, KeyError: 'Records'FunctionA test event or a message wrapped by SQS or SNS
The function runs over and overFunctionIt writes back into the bucket that triggers it

"Unable to validate the following destination configurations"

This is the error you get when you add or save a trigger and S3 can't confirm it's allowed to call the destination. Before it saves a notification configuration, S3 checks every destination in it, and for a Lambda function, it checks that the function's own permission policy, its resource-based policy, lets S3 invoke it from that bucket. Adding the trigger from the Lambda console sets that permission for you. Setting up the notification from the S3 side, the CLI, a script or infrastructure code doesn't, so you add it yourself:

aws lambda add-permission --function-name resize-photo \
  --statement-id s3invoke --action lambda:InvokeFunction \
  --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::shop-uploads \
  --source-account 123456789012

Use your function name, bucket name and account ID. The --source-account line matters: bucket names are global, and if you ever delete the bucket, someone else could create one with the same name. Limiting the permission to your account keeps their bucket from invoking your function. To see what the function allows today, read its policy and look for s3.amazonaws.com and your bucket's ARN:

aws lambda get-policy --function-name resize-photo --query Policy --output text

The same error appears in a second situation: the bucket still has an old trigger for a function that has since been deleted, or whose permission was removed. Every save validates every destination in the configuration, so one dead entry blocks all changes. To get unstuck:

  1. List the bucket's current configuration: aws s3api get-bucket-notification-configuration --bucket shop-uploads.
  2. Find any entry whose function no longer exists, or lacks the S3 permission.
  3. Write the configuration again without the dead entries, using the JSON in the next section. In the console, delete all the failed notifications at the same time.
  4. For each function you keep, run aws lambda add-permission as above, then save the configuration.

Jake: "So the bucket asks the function for permission before it agrees to call it?"

Ethan: "Every time the setting is saved. No permission on file, no trigger."

Save the trigger from the CLI

A notification configuration is a small JSON document. This one runs the function for every new .jpg under uploads/, however it was uploaded:

{
  "LambdaFunctionConfigurations": [
    {
      "Id": "resize-new-photos",
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:resize-photo",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {
        "Key": {
          "FilterRules": [
            { "Name": "prefix", "Value": "uploads/" },
            { "Name": "suffix", "Value": ".jpg" }
          ]
        }
      }
    }
  ]
}

Save it as notification.json and apply it:

aws s3api put-bucket-notification-configuration --bucket shop-uploads \
  --notification-configuration file://notification.json

Two things to know before you run it. It replaces the bucket's whole notification configuration, so include every trigger you want to keep. And the function must be in the same Region as the bucket. To remove every notification from a bucket, save an empty configuration:

aws s3api put-bucket-notification-configuration --bucket shop-uploads \
  --notification-configuration '{}'

Big files never trigger: Put vs CompleteMultipartUpload

This one cost Ethan an afternoon. S3 names an object-created event after the API call that created the object, and you subscribe to those names:

Event typeFires when an object is created by
s3:ObjectCreated:PutA single PUT request
s3:ObjectCreated:PostA browser form upload (POST)
s3:ObjectCreated:CopyA copy from another object
s3:ObjectCreated:CompleteMultipartUploadThe end of a multipart upload, including multipart copies
s3:ObjectCreated:*Any of the above

Many examples subscribe to s3:ObjectCreated:Put only. That works while you test with small files. But aws s3 cp and aws s3 sync switch to a multipart upload once a file reaches the CLI's multipart_threshold, which is 8 MB by default, and SDK upload helpers do the same at their own sizes. A multipart upload ends with CompleteMultipartUpload, so a Put-only trigger stays silent. You can watch the CLI make the switch on a 10 MB file:

aws s3 cp big-photo.jpg s3://shop-uploads/uploads/big-photo.jpg --debug 2>&1 | grep -o -m1 CreateMultipartUpload
CreateMultipartUpload

The fix is to subscribe to s3:ObjectCreated:*, as in the JSON above. In the console, that's the "All object create events" option.

Jake: "So my phone takes photos that are too good for your code?"

Ethan: "Too big for my trigger. Anything over 8 MB went up in pieces, and I was only listening for files that arrive in one piece."

Prefix and suffix filters that never match

Filters are plain text comparisons against the start and end of the object key, and they're stricter than they look:

  • No wildcards. *.jpg as a suffix matches nothing; use .jpg.
  • Case matters. Object keys are case-sensitive, so a .jpg suffix doesn't match IMG_2041.JPG. Many cameras write uppercase extensions, so either normalize names when you upload, or drop the suffix filter and check the extension in your code.
  • No leading slash. Keys don't start with /, so a prefix of /uploads/ never matches. Use uploads/.
  • Spaces and special characters. A space in a prefix or suffix must be written as +, and other special characters URL-encoded.

There's also a rule about overlap: two configurations on the same bucket can't use overlapping prefixes or suffixes for the same event type, so S3 refuses to save one that does. If you need two functions to see the same files, send the events to EventBridge or an SNS topic and fan out from there.

The trigger vanished: one configuration per bucket

A bucket has exactly one notification configuration, and every save replaces all of it. That's why triggers disappear without anyone deleting them: a second script, a teammate's console change or another tool saves its own configuration, and yours is gone. Terraform's aws_s3_bucket_notification resource works the same way, so only one of them can manage a bucket; two of them overwrite each other on every apply.

If several teams or tools need events from the same bucket, turn on S3's EventBridge integration instead and give each consumer its own EventBridge rule. Rules don't overwrite each other, and they can match on more than a prefix and suffix. If a rule doesn't fire, our EventBridge pattern guide walks through the usual mistakes.

Jake: "So my reports script could have switched off your photo trigger?"

Ethan: "If it saved its own notification on the same bucket, yes. That's why the photos and the reports now go through EventBridge."

Five minutes, at least once, and other timing facts

A few timing rules explain a lot of "it doesn't work" reports that fix themselves:

  • New or changed configurations take a while. After you create or change a notification configuration, it usually takes about five minutes to take effect. Upload a test file a minute after saving, and it can be missed.
  • Delivery is usually fast, not instant. Notifications typically arrive within seconds, but sometimes take a minute or longer.
  • At least once, in no guaranteed order. A rare retry can deliver the same event twice, and events can arrive out of order. Write the function so that processing the same file twice does no harm, for example by overwriting the same output key.
  • The function is invoked asynchronously. S3 doesn't wait for your function or show you its errors; the function's log and metrics are the only record.

So after saving or changing a trigger, test it in this order before you change anything else:

  1. Wait five minutes.
  2. Upload a new file whose key matches your prefix and suffix exactly, with no spaces in the name.
  3. Check the function's Invocations metric. If it's still zero, go back to the permission, event type and filters.
  4. If it ran, read the function's log for the error, then test a name with a space in it.

It ran, then failed: the object key is URL-encoded

Here's the handler most people write first, and the one Ethan shipped:

import boto3

s3 = boto3.client("s3")

def handler(event, context):
    record = event["Records"][0]
    bucket = record["s3"]["bucket"]["name"]
    key = record["s3"]["object"]["key"]
    obj = s3.get_object(Bucket=bucket, Key=key)
    return {"key": key, "size": obj["ContentLength"]}

It works for photo.jpg and fails for red flower.jpg, because the key in the event is URL-encoded: a space arrives as +, so the function asks S3 for red+flower.jpg, a different key that doesn't exist:

{"errorMessage": "An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.", "errorType": "NoSuchKey"}

Whether you see NoSuchKey depends on the function's role. With s3:ListBucket on the bucket, a missing key returns 404 NoSuchKey; without it, S3 returns 403 AccessDenied instead, which sends people off to fix permissions that were never the problem. Our NoSuchKey guide covers that 403-for-404 rule in detail.

The fixed handler decodes the key, handles every record in the event rather than only the first, and skips messages that aren't object events:

import urllib.parse
import boto3

s3 = boto3.client("s3")

def handler(event, context):
    if "Records" not in event:  # s3:TestEvent and other non-record messages
        print("Skipping non-record event:", event.get("Event"))
        return {"skipped": True}
    results = []
    for record in event["Records"]:  # one invocation can carry more than one record
        bucket = record["s3"]["bucket"]["name"]
        key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
        obj = s3.head_object(Bucket=bucket, Key=key)
        print(f"{record['eventName']} {key} {obj['ContentLength']} bytes")
        results.append(key)
    return {"processed": results}

In Node.js, the same decoding is decodeURIComponent after turning each + back into a space:

import { S3Client, HeadObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({});

export const handler = async (event) => {
  if (!event.Records) return { skipped: true }; // s3:TestEvent and other non-record messages
  const keys = [];
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
    const head = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
    console.log(`${record.eventName} ${key} ${head.ContentLength} bytes`);
    keys.push(key);
  }
  return { processed: keys };
};

Both versions turn red+flower.jpg back into red flower.jpg, and a name with a percent sign, such as 50% off.jpg, decodes correctly too.

KeyError: 'Records': the test event in an SQS or SNS pipeline

When you save a notification that sends to an SQS queue or SNS topic, S3 first sends a test message to check it can deliver. That message doesn't look like a normal event:

{
  "Service": "Amazon S3",
  "Event": "s3:TestEvent",
  "Time": "2026-10-13T20:10:00.000Z",
  "Bucket": "shop-uploads",
  "RequestId": "5582815E1AEA5ADF",
  "HostId": "EXAMPLE"
}

There's no Records list. If your function reads from that queue or topic and goes straight for event["Records"] inside the message, the very first message crashes it:

{"errorMessage": "'Records'", "errorType": "KeyError"}

With SQS in the middle, a crash like that can send the message back to the queue to be retried. The fix is the check at the top of the handler above: if there's no Records, log it and return. When S3 sends to SQS or SNS, the S3 event also sits inside the queue or topic message body as a JSON string, so parse that first. If S3 can't deliver to the queue at all, the queue's access policy is the usual cause; our SQS queue policy guide has the statement S3 needs.

Terraform and CloudFormation: create the permission first

In infrastructure code, "Unable to validate" usually means an ordering problem: the bucket notification was created before the Lambda permission existed, so S3's check failed. In Terraform, make the notification wait for the permission with depends_on:

data "aws_caller_identity" "current" {}

data "aws_lambda_function" "resize" {
  function_name = "resize-photo"
}

resource "aws_s3_bucket" "uploads" {
  bucket = "shop-uploads"
}

resource "aws_lambda_permission" "allow_bucket" {
  statement_id   = "s3invoke"
  action         = "lambda:InvokeFunction"
  function_name  = data.aws_lambda_function.resize.function_name
  principal      = "s3.amazonaws.com"
  source_arn     = aws_s3_bucket.uploads.arn
  source_account = data.aws_caller_identity.current.account_id
}

resource "aws_s3_bucket_notification" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  lambda_function {
    lambda_function_arn = data.aws_lambda_function.resize.arn
    events              = ["s3:ObjectCreated:*"]
    filter_prefix       = "uploads/"
    filter_suffix       = ".jpg"
  }

  depends_on = [aws_lambda_permission.allow_bucket]
}

CloudFormation has a version of the same problem: if the bucket, the function and the AWS::Lambda::Permission are in one template and refer to each other, you can end up with a circular dependency, because the bucket can't be created with its notification until the permission exists. The simplest way out is to create the stack without the notification configuration first, then update the stack to add it.

Same Region, and the loop to avoid

Two last rules. The function must be in the same Region as the bucket; a function in another Region can't be a notification destination, so pick the bucket's Region when you create it. And watch for the loop: if the function writes its output back into the bucket that triggers it, every output file triggers the function again. Ethan's resize step writes small copies, and a trigger on every new object would resize the resized copies, forever, with a bill to match. Use a second bucket for the output, or limit the trigger to an input prefix such as uploads/ and write the results under a different one, such as thumbnails/.

Jake: "A photo that makes a photo that makes a photo?"

Ethan: "Until someone notices the bill. The uploads/ prefix is what stops it."

Troubleshooting by symptom

Unable to validate the following destination configurations

Add the Lambda permission for s3.amazonaws.com, and remove any old triggers for deleted functions, then save again.

Small files trigger the function, large files don't

The trigger uses s3:ObjectCreated:Put. Large uploads are multipart. Change it to s3:ObjectCreated:*.

Nothing happens right after creating the trigger

New or changed configurations usually take about five minutes to take effect. Wait, then upload a new file.

NoSuchKey or AccessDenied for files with spaces in the name

Decode the key with urllib.parse.unquote_plus in Python, or decodeURIComponent after replacing + with a space in Node.js.

KeyError: 'Records'

A test event or a wrapped SQS or SNS message reached the handler. Skip events without Records, and parse the message body first.

The trigger disappeared

Another tool or person saved a new notification configuration on the bucket. Re-add yours, or move consumers to EventBridge.

The function runs in an endless loop

It writes into the bucket that triggers it. Write to another bucket or prefix, and filter the trigger on the input prefix.

S3 event notifications and Lambda: frequently asked questions

Why is my S3 event notification not triggering Lambda?

Usually a missing permission, a Put-only event type with multipart uploads, a filter that doesn't match, a replaced configuration, or the five-minute delay after a change.

What does "Unable to validate the following destination configurations" mean?

S3 checked the destination before saving and couldn't confirm it may invoke it. Add the Lambda permission for s3.amazonaws.com, or remove triggers for deleted functions.

How do I give S3 permission to invoke a Lambda function?

Run aws lambda add-permission with --principal s3.amazonaws.com, --action lambda:InvokeFunction, --source-arn of the bucket and --source-account of your account.

Why does my S3 trigger work for small files but not large ones?

Large files uploaded with aws s3 cp use multipart uploads from 8 MB by default, which fire s3:ObjectCreated:CompleteMultipartUpload, not Put. Use s3:ObjectCreated:*.

How long does it take for an S3 event notification to start working?

After you create or change the configuration, it usually takes about five minutes to take effect. Notifications then typically arrive within seconds.

Why does the S3 event key have plus signs instead of spaces?

The key in the event is URL-encoded, so spaces become +. Decode it with urllib.parse.unquote_plus in Python before calling S3.

Why does my Lambda get AccessDenied instead of NoSuchKey?

Without s3:ListBucket on the bucket, S3 returns 403 AccessDenied for a key that doesn't exist. Usually the key wasn't decoded.

What is s3:TestEvent?

A test message S3 sends when you save a notification to an SQS queue or SNS topic. It has no Records list, so handlers must skip it.

Can I use wildcards in S3 event notification filters?

No. A prefix or suffix is matched as plain text, so use .jpg, not *.jpg. Matching is case-sensitive.

Can two Lambda functions be triggered by the same S3 event?

Not with overlapping prefixes or suffixes for the same event type. Use EventBridge or an SNS topic to fan out to several functions.

Why did my S3 trigger disappear?

Saving a notification configuration replaces the whole configuration. Another tool, script or person likely saved theirs over yours.

How do I delete an S3 event notification?

Save the configuration without it. To remove all of them, run put-bucket-notification-configuration with an empty {} configuration.

Does the Lambda function have to be in the same Region as the S3 bucket?

Yes. The function must be in the same Region as the bucket to be a notification destination.

Can S3 send the same event twice?

Rarely, yes. Delivery is at least once and not guaranteed in order, so make your function safe to run twice on the same file.

How do I stop an S3 Lambda trigger from looping?

Write the output to a different bucket, or filter the trigger on an input prefix and write results under another prefix.

Why does Terraform fail with Unable to validate the following destination configurations?

The notification was created before the Lambda permission. Add depends_on pointing to the aws_lambda_permission resource.

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03) • CloudOps Engineer – Associate (SOA-C03).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. Objects land in the bucket but the Lambda function never runs. What permission is often missing?

The function's resource-based policy must allow s3.amazonaws.com to invoke it, scoped to the bucket with a source ARN and account condition. The console adds it for you; CLI and templates often forget.

2. Two event notifications with overlapping prefixes and suffixes fail to save. Why?

S3 rejects notification configurations whose filters overlap for the same event type.

3. A function writes its output back into the same bucket and runs forever. How do you prevent the loop?

Write outputs to another bucket or to a prefix excluded by the trigger's filter. Lambda can also detect and stop some recursive loops.

4. Many targets must react to the same upload. What is the cleanest design?

Send S3 events to Amazon EventBridge, or to an SNS topic that fans out to SQS queues and functions.

5. Do S3 event notifications guarantee exactly-once delivery?

No. Events are delivered at least once and can occasionally arrive late or more than once, so make the handler idempotent.

6. Uploads through multipart do not trigger an "ObjectCreated:Put" rule. Why?

Multipart uploads raise s3:ObjectCreated:CompleteMultipartUpload. Use s3:ObjectCreated:* to catch every method.

Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.

Ethan changed two things. The trigger now listens for s3:ObjectCreated:*, and the handler decodes every key and loops over every record. Jake's big phone photos went through on the next upload, the old ones were processed by re-copying them into uploads/, and the reports script now listens through EventBridge instead of fighting the photo trigger. Jake's verdict: "So the photos were fine. The trigger just didn't speak multipart."

📌 If you keep one line from this page

Give S3 permission with lambda add-permission, subscribe to s3:ObjectCreated:* so multipart uploads count, wait five minutes after a change, and decode the key before you call S3.

--principal s3.amazonaws.com · ObjectCreated:* · unquote_plus · one configuration per bucket.

Revision note. Written October 13, 2026, for S3 event notifications to AWS Lambda. "Many a little makes a mickle," says the Scottish proverb; on S3, a big file arrives in many little parts, and the trigger has to listen for that.

Related