A dollar a month per key does not sound like a line item worth investigating. That is exactly why KMS is easy to under-budget: the storage charge really is trivial, so nobody does the multiplication that turns a handful of keys into a few dozen, or notices that the application calling Decrypt on every request is paying a completely different meter than the one everybody remembers from the pricing page. Neither mistake looks reckless in the moment. Each key, each region, each API call is a perfectly reasonable decision on its own.
Why does KMS cost anything at all?
KMS has two charges that behave nothing like each other, plus a couple of add-ons worth knowing before they surprise you:
| What you pay for | Rate |
|---|---|
| Customer-managed key (symmetric, asymmetric, HMAC, imported key material, or a multi-Region primary/replica) | $1/month per key, prorated hourly |
| Automatic or on-demand key rotation | +$1/month for the first rotation, +$1/month for the second — no further charge after that |
Standard requests (Encrypt, Decrypt, GenerateDataKey, ReEncrypt, GenerateMac, VerifyMac, GenerateRandom) | $0.03 per 10,000 requests |
Asymmetric requests (Sign, Verify, GetPublicKey, Encrypt/Decrypt with RSA or ECC keys) | $0.15 per 10,000 requests |
| Free tier | 20,000 standard requests/month across all Regions |
| AWS CloudHSM key store | $1.60 per HSM per hour, on top of the $1/month key charge |
The storage side is genuinely flat: $1/month whether the key is used once or a million times. The usage side is not flat at all — it is a per-request meter with no ceiling, and it is priced separately depending on whether the operation is symmetric or asymmetric. Asymmetric operations also do not get the free tier: AWS explicitly excludes Sign, Verify, GetPublicKey, and GenerateDataKeyPair from the 20,000 free requests, so a signing-heavy workload starts paying from request one.
These are the rates AWS lists at the time of writing. AWS does not publish per-Region variation for KMS the way it does for compute, but it does change prices — check the KMS pricing page before you build a budget around these numbers.
Why is my bill bigger than "keys × $1" suggests?
Because almost nobody actually has "a few keys." The number that matters is keys times environments times Regions. A team with 8 services, each with its own customer-managed key, deployed to dev, staging, and prod, already has 24 keys — $24/month before a single API call. Add a second Region for disaster recovery and every replica is billed as its own full key at $1/month in the Region it lands in, not as a free pointer back to the primary — a multi-Region key is two (or more) separate billable resources wearing one name.
Rotation compounds the same way but tops out: the first two rotations each add $1/month per key, and every rotation after that is free, so a key rotated for two years costs at most $3/month in storage, not an ever-climbing annuity. That cap is easy to miss the first time you see the charge, because "rotation" sounds like it should be included in the $1 you are already paying.
The bigger driver is almost always the request meter, and it shows up fastest with envelope encryption — the pattern behind S3 SSE-KMS, EBS encryption, and most application-level encryption libraries, where every object write calls GenerateDataKey and every read calls Decrypt. Run the math on a moderately busy bucket: 50 GenerateDataKey calls a second, one per object written, is roughly 130 million calls a month. At $0.03 per 10,000 requests, that is about $390/month — for a bucket where the KMS key itself still shows up as $1/month in the console, because the two charges live on completely different lines in Cost Explorer.
A quieter driver sits next to it: AWS managed keys (the aws/service-named keys AWS creates automatically the first time you encrypt something with a given service) have no storage charge at all, but every API request against them bills at the same standard rate as a customer-managed key. Switching a workload from a customer-managed key to an AWS managed one saves the $1/month, not the request volume.
How do I find out what's actually driving the number?
Start in Cost Explorer, filtered to KMS and grouped by Usage Type. That split separates the flat per-key storage charge from the per-request charge in one screen, which tells you immediately whether you are chasing key sprawl or a chatty caller.
To list every customer-managed key with its state and creation date — AWS managed keys carry KeyManager: AWS and are worth filtering out, since you cannot delete or consolidate those anyway:
aws kms list-keys --query 'Keys[].KeyId' --output text | \
xargs -I{} aws kms describe-key --key-id {} \
--query '[KeyMetadata.KeyId,KeyMetadata.KeyManager,KeyMetadata.KeyState,KeyMetadata.CreationDate]' \
--output text
To find which key and which caller is generating the request volume, CloudTrail has every Decrypt and GenerateDataKey event with the identity behind it:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--start-time "$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)" \
--max-results 50 \
--query 'Events[].[EventTime,Username,Resources[0].ResourceName]' \
--output table
If one Lambda function or one bucket's key shows up dozens of times a minute, that is your $0.03-per-10,000 problem, not a key-count problem.
How do I bring the bill down without breaking anything?
Scheduling a KMS key for deletion is not instant — AWS enforces a 7-to-30-day waiting window you choose at schedule time, and you can cancel within that window. But once the window closes, the key is gone permanently, and any data still encrypted under it becomes permanently unreadable. Before scheduling deletion, confirm nothing references the key ARN in application config, and check the key's last-used date in CloudTrail rather than assuming it is safe because nobody remembers creating it.
Cache data keys instead of calling GenerateDataKey per object. This is the highest-leverage fix, because it attacks the meter that usually dwarfs everything else. The AWS Encryption SDK ships a data key caching feature specifically for this — reuse one data key across a batch of objects instead of round-tripping to KMS for every single one, and the request volume drops by whatever factor you batch by.
Delete customer-managed keys with no recent CloudTrail activity and no reference in code. Each one removed is $1/month gone immediately, plus whatever rotation surcharge it had accumulated.
Prefer AWS managed keys when you do not need key policies, cross-account sharing, or manual rotation control. They cost nothing to store, and for a single-account workload with default security requirements, a customer-managed key is often solving a compliance problem you do not have.
Drop multi-Region replicas in Regions nothing actually reads from. Disaster-recovery replication is a real reason to pay for a second key; "we might expand there someday" is not — you can always create the replica when the expansion happens.
Consolidate one-key-per-microservice sprawl where compliance allows it. A shared key with a tightly scoped key policy and per-principal grants covers most internal services without paying $1/month per service just to keep them logically separate.
What else should I check while I'm in here?
If you switched Secrets Manager from its default AWS managed key to a customer-managed KMS key — usually to get cross-account sharing or a custom key policy — every GetSecretValue call now triggers a KMS Decrypt call behind the scenes, stacking KMS's $0.03-per-10,000 request charge on top of Secrets Manager's own $0.05-per-10,000 API charge for the same call. See how much does AWS Secrets Manager cost for the rest of that bill, including the caching fix that cuts both meters at once.
If a Lambda function behind API Gateway calls Decrypt directly on every invocation instead of relying on Lambda's automatic environment-variable decryption at cold start, that function is now paying three per-request meters stacked on top of each other for the same request: the API Gateway charge, the KMS charge, and whatever downstream call the decrypted value was for. See what API Gateway costs at scale for how the first of those three adds up on its own.
If GuardDuty is exporting findings to S3 for retention or a SIEM pipeline, that export requires a KMS key you control — GuardDuty is granted kms:GenerateDataKey against it to encrypt every export batch. The request volume is trivial next to a chatty application (exports run on a schedule, not per finding), but it's one more key with one more cross-service grant to track down when you're auditing what actually holds a grant on a given key. See what GuardDuty costs once it's on everywhere for the rest of that bill.
More broadly, a flat-looking per-resource charge that turns out to hide a much larger per-request one is a pattern that shows up across an AWS bill, not just in KMS — the wider account-wide sweep of where that pattern repeats works through the rest of it service by service.
How do I catch this before it becomes a real number?
KMS cost almost never arrives as a spike. It arrives as one more key added for one more environment, one more Region replicated "just in case," one more service that starts calling Decrypt on the request path instead of caching the result — each one too small to notice on its own, until the total is something somebody has to explain in a cost review.
Connect your AWS account read-only, and Parsivex's daily anomaly checks compare every service's spend — KMS included — against its own trailing baseline, so a genuine jump (a caching regression that starts calling GenerateDataKey per request instead of per batch, a key rotated into an unexpected surcharge) gets flagged the next morning instead of buried in a monthly total. For how those daily checks and severity thresholds work, see cost anomaly alerts, or read how scans work for what a connected account scan reads across the rest of your bill.