AWS gives you two places to keep sensitive values: AWS Secrets Manager and the Parameter Store capability of AWS Systems Manager. Both encrypt with KMS, both are controlled by IAM, and both log reads to CloudTrail, so teams often pick one by habit and only discover the differences when they hit a throughput limit, a cross-account requirement, or a bill. The services were built for different jobs. Parameter Store is a hierarchical configuration store that can hold encrypted values; Secrets Manager is a credential lifecycle service with versioning, rotation and replication.

This article compares them on storage, cost, throughput, cross-account and cross-Region access, and integration, with real code and a worked cost example, and ends with a decision rule you can apply to each value you store. Figures were checked against AWS documentation and pricing pages in October 2026; prices vary by Region, so confirm them for yours.

Advertisement

Side by side

PropertyParameter StoreSecrets Manager
Primary purposeConfiguration, hierarchical by pathCredentials and their lifecycle
Value size4 KB standard, 8 KB advanced65,536 bytes
Count per account and Region10,000 standard, 100,000 advancedHigh; check Service Quotas
Storage priceStandard free; advanced $0.05 per parameter-month$0.40 per secret-month
API priceFree at default throughput; $0.05 per 10,000 parameter interactions with higher throughput$0.05 per 10,000 calls
Read throughput40 TPS default shared by GetParameter calls; up to 10,000 with higher throughputAbout 10,000 GetSecretValue per second per Region; check Service Quotas
RotationNone built inManaged rotation or a rotation Lambda
Cross-accountAdvanced parameters shared through AWS RAMResource-based policy on the secret
Cross-RegionNone; copy values yourselfReplica secrets in other Regions
ExtrasVersion labels; parameter policies (expiry, notifications) on advanced tierStaging labels, random password generation

Two rows decide most choices. Throughput: the Parameter Store default is low enough that a fleet reading at startup can be throttled. Lifecycle: if a value must rotate, cross accounts by resource policy, or exist in several Regions, Secrets Manager does that natively and Parameter Store does not.

How each stores and versions values

Parameter Store holds three types: String, StringList and SecureString. SecureString values are encrypted with a KMS key, either the AWS managed aws/ssm key or a customer managed key you specify, and decrypted only when the caller asks for WithDecryption and holds kms:Decrypt. Names are paths such as /payments/prod/db/host, so GetParametersByPath loads a service's whole subtree, and IAM policies can be scoped to a path prefix. Every write creates a new version, and labels such as release can point at a version.

Secrets Manager stores a secret as a string or binary, conventionally a JSON object such as username, password, host and port, encrypted with aws/secretsmanager or a customer managed key. Versions carry staging labels: AWSCURRENT is what readers get by default, AWSPREVIOUS the prior value, and AWSPENDING a value being introduced by rotation. That three-label protocol is what lets rotation change a password without breaking clients mid-flight; the rotation deep dive walks through each step. If you need rotation at all, that is the strongest reason to choose Secrets Manager. For the underlying encryption model, see KMS envelope encryption.

Advertisement

Reading values without hammering either API

Neither service should be on the hot path of every request. Read at startup or through a cache with a time-to-live, and refresh on authentication failure. Two practical patterns:

In application code, cache values with a TTL. AWS publishes caching clients for Secrets Manager in several languages (for Python, aws-secretsmanager-caching); a small wrapper does the same for Parameter Store:

import json, time, boto3

ssm, sm = boto3.client("ssm"), boto3.client("secretsmanager")
_cache = {}

def _cached(key, ttl, load):
    hit = _cache.get(key)
    if hit and time.monotonic() - hit[1] < ttl:
        return hit[0]
    value = load()
    _cache[key] = (value, time.monotonic())
    return value

def config(prefix="/payments/prod/", ttl=300):
    def load():
        out, kw = {}, {"Path": prefix, "Recursive": True, "WithDecryption": True}
        for page in ssm.get_paginator("get_parameters_by_path").paginate(**kw):
            out.update({p["Name"][len(prefix):]: p["Value"] for p in page["Parameters"]})
        return out
    return _cached(("ssm", prefix), ttl, load)

def db_credentials(secret_id="payments/prod/db", ttl=300):
    return _cached(("sm", secret_id), ttl,
                   lambda: json.loads(sm.get_secret_value(SecretId=secret_id)["SecretString"]))

def connect():
    try:
        return open_db(**db_credentials())
    except AuthError:
        _cache.pop(("sm", "payments/prod/db"), None)   # rotated: refetch once
        return open_db(**db_credentials())

In Lambda, the AWS Parameters and Secrets Lambda Extension runs a local HTTP cache on port 2773. Requests carry the function's session token in an X-Aws-Parameters-Secrets-Token header. TTLs are set by SECRETS_MANAGER_TTL and SSM_PARAMETER_STORE_TTL (default and maximum 300 seconds), and the cache holds up to 1,000 items by default:

import json, urllib.request, urllib.parse, boto3

def _get(path):
    token = boto3.Session().get_credentials().get_frozen_credentials().token
    req = urllib.request.Request("http://localhost:2773" + path)
    req.add_header("X-Aws-Parameters-Secrets-Token", token)
    return json.loads(urllib.request.urlopen(req).read())

def handler(event, context):
    secret = _get("/secretsmanager/get?secretId=" + urllib.parse.quote("payments/prod/db", safe=""))
    creds = json.loads(secret["SecretString"])
    flag = _get("/systemsmanager/parameters/get?name=" + urllib.parse.quote("/payments/prod/feature_x", safe=""))
    return {"feature_x": flag["Parameter"]["Value"], "user": creds["username"]}

The extension does not notice a changed value before its TTL expires, so code that uses rotated credentials still needs the retry-on-auth-failure path. AWS documents resolving the token from the SDK credential chain inside the handler rather than reading AWS_SESSION_TOKEN, because SnapStart functions do not set that variable.

Platform integrations

Most AWS compute services can inject values from either store, so application code may never call the APIs directly:

  • ECS task definitions accept secrets entries whose valueFrom is a Parameter Store parameter or a Secrets Manager secret ARN; the agent resolves them at task start, using the task execution role.
  • CloudFormation dynamic references {{resolve:ssm:name}}, {{resolve:ssm-secure:name}} and {{resolve:secretsmanager:id:SecretString:key}} resolve at deploy time. Only some resource properties support ssm-secure.
  • References between the stores. Calling Parameter Store with a name under /aws/reference/secretsmanager/ returns a Secrets Manager secret, so an application that only knows Parameter Store paths can still read rotated credentials.
  • Kubernetes typically uses an external secrets operator or the Secrets Store CSI driver with the AWS provider, both of which support either service.

Injection at start is simple, but a value injected as an environment variable does not change when the secret rotates. Containers must be restarted, or the application must read through a cache instead.

Access control, cross-account and cross-Region

Grant least privilege by naming. Parameter Store ARNs include the path, so a role can read one service's subtree:

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow",
     "Action": ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
     "Resource": "arn:aws:ssm:eu-west-1:111122223333:parameter/payments/prod/*"},
    {"Effect": "Allow",
     "Action": "secretsmanager:GetSecretValue",
     "Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:payments/prod/*"},
    {"Effect": "Allow", "Action": "kms:Decrypt",
     "Resource": "arn:aws:kms:eu-west-1:111122223333:key/<payments-key-id>",
     "Condition": {"StringEquals": {"kms:ViaService": [
         "ssm.eu-west-1.amazonaws.com", "secretsmanager.eu-west-1.amazonaws.com"]}}}
  ]
}

The kms:ViaService condition limits decryption to calls made through the two services, which the IAM conditions guide explains with other useful keys. Secrets Manager ARNs end in a random six-character suffix, which is why the resource uses a wildcard.

Cross-account. A Secrets Manager secret takes a resource-based policy naming the consuming account's role. The secret must be encrypted with a customer managed key whose key policy also lets that role decrypt; the AWS managed key cannot be shared. Parameter Store shares advanced-tier parameters through AWS Resource Access Manager with read-only permission sets, and the consumer still needs access to the KMS key for SecureString values. Cross-Region. Secrets Manager can replicate a secret to other Regions and keeps replicas in sync with the primary, including rotated values. Parameter Store has no replication, so disaster recovery means writing values in both Regions from your deployment pipeline.

Worked example: splitting a payments service

A payments service runs 60 ECS tasks across two Regions. It has 40 configuration values (endpoints, timeouts, feature flags), 4 credentials (database, card processor API key, signing key, webhook secret), and deploys several times a day.

Everything in Secrets Manager. 44 values × 2 Regions × $0.40 is $35.20 a month in storage. If each task reads every value at start and refreshes every five minutes, that is 60 × 44 × 12 × 24 × 30, about 22.8 million calls a month, or roughly $114. Batching with BatchGetSecretValue cuts call count, but the design is paying credential prices for timeouts.

Split by purpose. The 40 configuration values go to standard Parameter Store parameters, free, loaded with GetParametersByPath every five minutes. That call returns at most 10 parameters per page, so each refresh is 4 calls: 60 tasks × 4 × 12 per hour is 0.8 calls per second, far under the 40 TPS default. A deploy that restarts all 60 tasks within seconds creates a burst, so startup jitter and retry with back-off still matter. The 4 credentials go to Secrets Manager with replicas in the second Region and rotation enabled: 4 × 2 × $0.40 is $3.20, plus about 2 million cached reads, roughly $10. Total: about $14 a month instead of about $149, and only the values that need rotation pay for it.

A decision rule per value

Decide per value, not per team. The rule below is the one the worked example applied, written as code so it can run over an inventory export and flag values in the wrong store:

def choose_store(v):
    # v: dict describing one value from your inventory
    if v["rotates"] or v["cross_region"]:
        return "secretsmanager"
    if v["cross_account"]:
        # both can share; Secrets Manager also covers rotation later
        return "secretsmanager" if v["is_credential"] else "ssm-advanced"
    if v["size_bytes"] > 8192:
        return "secretsmanager"                 # up to 65,536 bytes
    if v["size_bytes"] > 4096 or v["needs_expiry_policy"]:
        return "ssm-advanced"
    if v["is_credential"]:
        return "ssm-securestring"               # acceptable only if it truly never rotates
    return "ssm-standard"

The ssm-securestring branch is a deliberate compromise. A static API key that the vendor never rotates is safe enough as an encrypted parameter, and it costs nothing. The moment someone asks how you would rotate it after a leak, move it: rotation is a property you need on the worst day, not the average one.

Expect a few values to resist classification. A TLS private key is a credential but may exceed 8 KB with its chain; an OAuth client secret might be shared with a partner account. Classify by the strictest requirement the value has, and record the reason next to it so the next review does not have to rediscover it.

Auditing and operating both stores

Both services write management and read events to CloudTrail, so GetSecretValue and GetParameter calls are attributable to a role. Use that in three ways:

  • Detect unexpected readers. Alert when a credential is read by a principal outside its expected set, or from outside your network, by filtering CloudTrail events by secret ARN or parameter path.
  • Find dead values. A secret nobody has read in 90 days is either unused or read through a path you did not know about. Secrets Manager records a last-accessed date per secret; for parameters, query CloudTrail.
  • Watch throttling. Count ThrottlingException errors per service in CloudWatch metrics or logs; a rising rate is the early warning before a deploy fails.

Advanced parameters add parameter policies: Expiration deletes a parameter at a set time, ExpirationNotification emits an EventBridge event before that, and NoChangeNotification fires if a value has not changed for a set period. They are a lightweight way to force review of values that should not live forever, such as temporary partner tokens, without adopting Secrets Manager rotation for them. Protect both stores' KMS keys with the same care as the values: once a key is deleted at the end of its waiting period, every value it encrypted is unrecoverable, as the KMS guide explains.

Failure modes

  • ThrottlingException at deploy. A fleet hits the 40 TPS Parameter Store default on startup. Add jitter and caching first, then enable higher throughput if needed, remembering it is billed per call and applies account-wide in the Region.
  • Stale credentials after rotation. Values injected as environment variables or cached without a refresh path break when the old password is retired. Read through a cache and refetch on authentication failure.
  • Decrypt denied. The role can read the parameter or secret but not use the customer managed KMS key; the error names KMS, not the store.
  • Cross-account read fails. The secret uses the AWS managed key, which other accounts cannot use. Re-encrypt with a customer managed key.
  • Advanced tier surprise. Changing a standard parameter to advanced is one-way; moving back means deleting and recreating it.
  • Secrets in plain String parameters. Anyone with read access, and every log that prints the response, sees the value. Audit for credentials stored as String.

What to do next

  1. Inventory every value you store and label it configuration or credential.
  2. Move configuration into Parameter Store paths per service and environment, as standard parameters unless you need 8 KB values, policies or RAM sharing.
  3. Move credentials that rotate, cross accounts or need replicas into Secrets Manager, encrypted with a customer managed key.
  4. Scope IAM by path prefix and add kms:ViaService to decrypt permissions.
  5. Read through a TTL cache or the Lambda extension, and refetch on authentication failure.
  6. Add jitter to fleet startup and alarm on throttling errors from both services.
  7. Turn on rotation for each credential and test a rotation in staging while load is running.
Key takeaway: Parameter Store is a hierarchical configuration store that can encrypt values; Secrets Manager manages credentials through versions, rotation, resource policies and replicas. Put configuration in Parameter Store, where standard parameters are free but default throughput is 40 TPS, and put credentials that rotate, cross accounts or need multiple Regions in Secrets Manager. Read both through a cache with a refresh path, scope IAM by path and KMS key, and plan for throttling at deploy time.