S3 event notifications to Lambda
S3 invokes a Lambda function asynchronously when objects change. What S3 sends and what Lambda expects, the resource policy the S3 API does not create for you, the event types and filters that silently miss events, the recursion trap, a known-good setup and known-bad ones with their symptoms.
markdownpatternv4VMG0-X0MFstage: buildtrust: officialservice: s3service: lambdainterface: cliinterface: iacS3 event notificationS3 trigger Lambdalambda add-permissionput-bucket-notification-configurationObjectCreatedCompleteMultipartUploadUnable to validate the following destination configurationsURL-encoded keyrecursive invocation
Two policies are involved and only one is created for you. The bucket's notification configuration says which events go to the function; the function's resource-based policy must allow s3.amazonaws.com to invoke it. The Lambda console adds that permission when you create the trigger there. Configuring the notification through the S3 API, CLI or most templates does not, and S3 then refuses to save the configuration.
What crosses the handoff
| Side | Emits / expects |
|---|---|
| S3 | An asynchronous invocation with a Records[] array. Each record has eventName (for example ObjectCreated:Put), s3.bucket.name, s3.object.key (URL-encoded: spaces arrive as +), size, eTag. Since July 2026, events also carry the bucket's system-generated tags. |
| Lambda | A resource-based policy statement allowing lambda:InvokeFunction by principal s3.amazonaws.com, scoped with aws:SourceArn (the bucket ARN) and aws:SourceAccount (the bucket owner). The function's execution role then needs s3:GetObject (and kms:Decrypt for SSE-KMS objects) to read what triggered it. |
Mismatches that bite
- Multipart uploads emit a different event. A large upload produces
s3:ObjectCreated:CompleteMultipartUpload, nots3:ObjectCreated:Put. Subscribe tos3:ObjectCreated:*. - Filters are literal. Prefix and suffix filters do not accept
*, and some special characters must be URL-encoded in the filter (test=abc/becomestest%3Dabc/). - Keys arrive URL-encoded. Decode before calling
GetObject(in Node.js:decodeURIComponent(key.replace(/\+/g, " "))), or keys with spaces and symbols returnNoSuchKey. - Batches.
Recordsis an array; handle every record. - Retries. Invocation is asynchronous: Lambda retries failed invocations, so the handler must be idempotent.
- Recursion. A function that writes into the bucket and prefix that trigger it invokes itself repeatedly. Write to a different bucket, or to a prefix the notification does not cover.
- Throttling. Bursts beyond the function's concurrency are throttled; size concurrency for the bucket's write rate.
- Destinations other than Lambda (SNS topics, SQS queues) encrypted with AWS managed keys (
aws/sns,aws/sqs) cannot receive notifications; use a customer managed key that allowss3.amazonaws.comkms:GenerateDataKeyandkms:Decrypt. - Only general purpose buckets support event notifications (not directory or table buckets).
Known-good
- Allow S3 to invoke the function (function's account):
aws lambda add-permission --function-name <function> \
--statement-id s3-invoke-<bucket> --action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::<bucket> --source-account <bucket-owner-account-id>
- Point the bucket at it:
aws s3api put-bucket-notification-configuration --bucket <bucket> --notification-configuration '{
"LambdaFunctionConfigurations": [{
"LambdaFunctionArn": "<function ARN>",
"Events": ["s3:ObjectCreated:*"],
"Filter": { "Key": { "FilterRules": [{ "Name": "prefix", "Value": "incoming/" }] } }
}]
}'
Since 2026-08-25 the same grant can instead live in a full resource-based policy written with put-resource-policy. That call replaces the whole function policy, including grants made with add-permission, so use one style per function (reference/lambda/permissions.md).
- Execution role:
s3:GetObjectonarn:aws:s3:::<bucket>/incoming/*,kms:Decryptif the bucket uses SSE-KMS, and CloudWatch Logs permissions. Outputs go to another bucket or prefix.
put-bucket-notification-configuration replaces the bucket's whole notification configuration: read the current one first and merge, or you delete other subscriptions (the API documentation: "This action replaces the existing notification configuration with the configuration you include in the request body").
Known-bad
| Configuration | Symptom |
|---|---|
Function policy replaced with put-resource-policy that omits the S3 statement | S3 stops invoking; the next notification change fails validation |
Notification set with the S3 API before add-permission | Unable to validate the following destination configurations when saving |
| Function deleted, notification left on the bucket | Same validation error on the next change to the bucket's notifications |
Events ["s3:ObjectCreated:Put"] only | Small uploads trigger; large (multipart) uploads never do |
Prefix filter uploads/* | No events at all: the * is matched literally |
Handler uses the raw s3.object.key | NoSuchKey for keys containing spaces or symbols |
| Function writes output into the triggering prefix | Runaway invocations and cost |
Execution role missing kms:Decrypt on an SSE-KMS bucket | Invocations happen but GetObject returns 403 |
Which side is wrong?
- Saving the notification fails with the validation error → the destination side: missing or stale function policy, deleted function, or KMS on an SNS/SQS destination. The
AWSSupport-TroubleshootS3EventNotificationsSystems Manager runbook checks this when the bucket and runbook are in the same account. - Saved, but the function never runs → the S3 side: event types and filters. Then Lambda throttling.
- The function runs and fails reading the object → the execution role or the key:
guides/s3/fix-403.md.
Buffering through SQS or SNS instead
For bursts, retries per object, or several consumers, send S3 notifications to an SQS queue, or to an SNS topic that fans out to queues, and trigger Lambda from the queue. Use a customer managed key or SSE-SQS, not aws/sqs (see above). Set up the queue side with patterns/sqs-to-lambda.md and patterns/sns-to-sqs-fanout.md.