---
cuppy_hub: aws
cuppy_path: patterns/s3-to-lambda.md
cuppy_section: docs
cuppy_doc: VMG0-X0MF
cuppy_doc_uuid: e6d4d33c-d194-44ba-9fa9-c3aea21cb4e8
cuppy_permalink: https://cuppy.io/aws/d/VMG0-X0MF
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/s3-to-lambda
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 4743f02ff1f106901bdafc981640495b52ac3cf9c7ba657564d64d3ef17d65c0
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# S3 event notifications to Lambda

**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`, 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>
```

2. 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`).

3. 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

| 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-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`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
