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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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

SideEmits / expects
S3An 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.
LambdaA 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, not s3:ObjectCreated:Put. Subscribe to s3:ObjectCreated:*.
  • Filters are literal. Prefix and suffix filters do not accept *, and some special characters must be URL-encoded in the filter (test=abc/ becomes test%3Dabc/).
  • Keys arrive URL-encoded. Decode before calling GetObject (in Node.js: decodeURIComponent(key.replace(/\+/g, " "))), or keys with spaces and symbols return NoSuchKey.
  • Batches. Records is 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 allows s3.amazonaws.com kms:GenerateDataKey and kms:Decrypt.
  • Only general purpose buckets support event notifications (not directory or table buckets).

Known-good

  1. 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>
  1. 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).

  1. Execution role: s3:GetObject on arn:aws:s3:::<bucket>/incoming/*, kms:Decrypt if 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

ConfigurationSymptom
Function policy replaced with put-resource-policy that omits the S3 statementS3 stops invoking; the next notification change fails validation
Notification set with the S3 API before add-permissionUnable to validate the following destination configurations when saving
Function deleted, notification left on the bucketSame validation error on the next change to the bucket's notifications
Events ["s3:ObjectCreated:Put"] onlySmall uploads trigger; large (multipart) uploads never do
Prefix filter uploads/*No events at all: the * is matched literally
Handler uses the raw s3.object.keyNoSuchKey for keys containing spaces or symbols
Function writes output into the triggering prefixRunaway invocations and cost
Execution role missing kms:Decrypt on an SSE-KMS bucketInvocations 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-TroubleshootS3EventNotifications Systems 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.

Read this document as markdown — the same bytes an agent receives.