Home / AWS

AWSHow-toAdvanced

S3 SSE-KMS driving up KMS costs or throttling: enable Bucket Keys safely

High-volume S3 workloads with SSE-KMS can cause big KMS bills and throttling. Find the busy buckets, enable S3 Bucket Keys, check encryption context dependencies, then handle existing objects and verify the drop.

Deepak Bhardwaj6 min read

Applies toAmazon S3 SSE-KMS, AWS KMS, AWS CloudTrail, AWS CLI v2

When to use this

You have one of these problems:

  • KMS appears high on the bill, and most of it is API requests rather than keys.
  • S3 requests fail under load with 503 SlowDown or KMS throttling errors.
  • CloudTrail is full of Decrypt and GenerateDataKey events.

With plain SSE-KMS, S3 calls KMS for each object. Writes need GenerateDataKey, and reads need Decrypt. Data lake jobs that touch millions of small files can make millions of KMS calls.

An S3 Bucket Key changes this. KMS creates a short-lived bucket-level key, and S3 uses it to create the object keys. S3 makes far fewer calls to KMS. AWS says this can cut request costs by up to 99%, but your result depends on the workload.

Step 1: confirm that KMS is the cost

Get KMS costs by usage type:

aws ce get-cost-and-usage \
  --time-period Start=2026-10-01,End=2026-11-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost UsageQuantity \
  --filter '{"Dimensions":{"Key":"SERVICE","Values":["AWS Key Management Service"]}}' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE \
  --query 'ResultsByTime[0].Groups[].{type:Keys[0], cost:Metrics.UnblendedCost.Amount, qty:Metrics.UsageQuantity.Amount}' \
  --output table

A usage type ending in KMS-Requests points to API calls. Keys themselves are usually a smaller part of the bill.

Step 2: find which buckets make the calls

If you have a CloudTrail table in Athena, count calls by bucket. KMS events usually record the bucket and key in the encryption context:

SELECT
    eventname,
    json_extract_scalar(requestparameters, '$.encryptionContext["aws:s3:arn"]') AS s3_arn,
    count(*) AS calls
FROM cloudtrail_logs
WHERE eventsource = 'kms.amazonaws.com'
  AND eventname IN ('Decrypt', 'GenerateDataKey')
  AND useridentity.invokedby = 's3.amazonaws.com'
  AND eventtime >= '2026-11-01'
GROUP BY 1, 2
ORDER BY calls DESC
LIMIT 50;

Your table's name, columns and partitions may differ. The ARN here is at object level, so you may want to group by the bucket portion of the string. Use a short time range, as CloudTrail volumes can be huge. If you have no table yet, use the CloudTrail cheat sheet and a sample period.

Step 3: check the current setting

BUCKET=my-data-lake

aws s3api get-bucket-encryption --bucket "$BUCKET" \
  --query 'ServerSideEncryptionConfiguration.Rules[0]'

aws s3api head-object --bucket "$BUCKET" --key path/to/file.parquet \
  --query '{sse:ServerSideEncryption, key:SSEKMSKeyId, bucketKey:BucketKeyEnabled}'

Check both, because the bucket default and the actual object can disagree. A request can also specify its own encryption headers, which override the default.

Step 4: check what depends on the encryption context

This is the part that can break things. With a Bucket Key, the encryption context changes from the object ARN to the bucket ARN.

Without a Bucket Key With a Bucket Key
aws:s3:arn in the context arn:aws:s3:::bucket/path/file arn:aws:s3:::bucket

Anything that checks object paths in the KMS context can be affected:

  • KMS key policies or IAM policies with kms:EncryptionContext:aws:s3:arn conditions.
  • Alerts and audit reports that expect object ARNs in KMS CloudTrail events.

Look for these conditions first:

aws kms get-key-policy --key-id "$KEY_ID" --policy-name default --output text | grep -n "EncryptionContext"

Also search your IAM policies, ideally in your infrastructure code. A condition such as this one must be widened to allow the bucket ARN, or requests will get AccessDenied:

"Condition": {
  "StringLike": {
    "kms:EncryptionContext:aws:s3:arn": [
      "arn:aws:s3:::my-data-lake",
      "arn:aws:s3:::my-data-lake/*"
    ]
  }
}

Also expect less detail in CloudTrail. You won't see a KMS event per object any more, so per-object KMS auditing stops. Use S3 data events if you need object-level records.

Step 5: enable it for new objects

Test on a non-production bucket first.

aws s3api put-bucket-encryption --bucket "$BUCKET" \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:ap-south-1:111122223333:key/your-key-id"
      },
      "BucketKeyEnabled": true
    }]
  }'

Then write and check a test object:

echo test > /tmp/bk-test.txt
aws s3api put-object --bucket "$BUCKET" --key bk-test/bk-test.txt --body /tmp/bk-test.txt \
  --query '{sse:ServerSideEncryption, bucketKey:BucketKeyEnabled}'
aws s3api get-object --bucket "$BUCKET" --key bk-test/bk-test.txt /tmp/bk-out.txt

Note that the bucket setting only affects objects written from now on. It doesn't require downtime, and existing objects stay readable.

Tip

Apps that pass encryption headers on each request can bypass the bucket setting. Those requests need x-amz-server-side-encryption-bucket-key-enabled: true, or the SDK equivalent. If your new objects show no BucketKeyEnabled, check the writer's settings.

Step 6: decide about existing objects

Objects already written keep making a KMS call for each read. To convert them, you have to copy them onto themselves. That makes sense only if they're read often.

For a single object:

aws s3api copy-object --bucket "$BUCKET" --key path/file.parquet \
  --copy-source "$BUCKET/path/file.parquet" \
  --server-side-encryption aws:kms \
  --ssekms-key-id "$KEY_ARN" \
  --bucket-key-enabled

At scale, use S3 Batch Operations with a manifest from S3 Inventory. Inventory can include the Bucket Key status, so it can also show how much of the bucket is left.

Caution

Copying in place creates a new version in versioned buckets, which adds storage. It costs requests, changes the LastModified date, can reset lifecycle rule ages and may trigger event notifications and replication. Pipelines that watch for new objects might reprocess everything. Test it on a prefix, and consider how soon the data would age out by itself.

Step 7: verify the drop

Compare KMS calls before and after, under similar load:

aws cloudwatch get-metric-statistics \
  --namespace AWS/Usage \
  --metric-name CallCount \
  --dimensions Name=Service,Value=KMS Name=Type,Value=API Name=Resource,Value=GenerateDataKey Name=Class,Value=None \
  --start-time 2026-11-01T00:00:00Z --end-time 2026-11-07T00:00:00Z \
  --period 86400 --statistics Sum

Run it again with Resource=Decrypt. Remember that this covers the whole account and region, so other services count too.

If you're still throttled

Bucket Keys help most, but they aren't the only factor:

Cause Action
Other services use the same KMS key heavily Check the KMS request quota for your region, and request an increase
Writers bypass the setting Fix their headers
Old objects are read at high rates Convert the hot prefixes
Bucket request rate is the real problem See S3 503 SlowDown
An access problem, not a limit See KMS access denied

Retry ThrottlingException with backoff, as the AWS SDKs do by default.

Prevent it next time

  • Set BucketKeyEnabled by default in the bucket module of your infrastructure code.
  • Use AWS Config or a policy check to flag SSE-KMS buckets without it.
  • Before you choose SSE-KMS for a high-volume bucket, check whether you need KMS at all. SSE-S3 has no KMS charges. See key strategy.
  • Review the KMS line on the bill each month, as part of your cost programme.