Best Place to Store Client API Credentials: Secrets Manager → SSM → DynamoDB (Cost & Scale)

StackAdvisor.AI / AI Tool

Many AWS teams start with what seems like an obvious choice — AWS Secrets Manager — to store API credentials, client tokens, or OAuth keys securely. It’s managed, encrypted, integrates with IAM and KMS, and supports rotation events out of the box. On paper, it’s the perfect solution.

But as one engineering team discovered in a real-world project shared within the AWS community, what begins as a best practice can turn into a recurring cost and scalability concern.

Their architecture handled OAuth2 client credentials for dozens of customer integrations, each requiring frequent token refresh and access operations. Over time, a growing Secrets Manager bill and throttling issues in the alternative (SSM Parameter Store) prompted a deeper evaluation.

This post shares their real-world evolution:

Secrets Manager → SSM Parameter Store → DynamoDB

This post summarizes that journey, the reasoning behind each migration step, and the technical design patterns that emerged from it — valuable lessons for any AWS DevOps or developer team facing similar access patterns.

Problem Statement: The OAuth2 Credential Challenge

The use case:

  • Hundreds of OAuth2 clients
  • Each with: client ID, client secret, access token, refresh token
  • Frequent reads (per request)
  • Writes during token refresh (every 60–120 mins per client)

Secrets Manager: Secure, but Expensive

Secrets Manager pricing:

$0.40 per secret per month × 130 clients = $52/month + $0.05 per 10,000 API calls

For high-frequency workloads, it’s a silent cost multiplier.

While the features like rotation events and secret versioning were robust, they weren’t needed for this specific OAuth token use case. The result was paying for features that were rarely used.

Let's look at how we would evolve this into a scalable and cost-effective solution.

SSM Parameter Store: Cheaper, but Throttled

SSM Parameter Store felt ideal — free in the Standard tier, KMS integrated, and simple to use.

Until we hit throttling limits:

OperationTPS LimitImpact
GetParameter40 TPSRead throttling at scale
PutParameter3 TPSToken refresh failures

Once concurrent ECS jobs kicked in, SSM’s rate limits turned into an operational bottleneck — retries, delays, and timeouts might become norm.

Understanding the Migration Path

StageStoreStrengthLimitation
1️⃣Secrets ManagerFeature-richExpensive for frequent access
2️⃣SSM Parameter StoreFree (Standard)Throttled under load
3️⃣DynamoDBScalable, fastRequires schema design

DynamoDB as a Credential Store

Schema Design

We can model our credentials as items under a composite key (PK, SK) pattern:

{
  "PK": "CLIENT#1234",
  "SK": "TOKEN#ACCESS",
  "access_token": "ya29.a0AfH6S...",
  "refresh_token": "1//0fA...",
  "expires_at": 1736216400,
  "last_updated": "2025-11-07T09:30:00Z",
  "ttl": 1736216400
}

Partition Key (PK) → client ID Sort Key (SK) → token type (access/refresh) TTL → auto-delete expired items

This gives us:

  • Fast lookups (GetItem by PK/SK)
  • Safe concurrent updates (ConditionExpression)
  • Automatic cleanup via TTL

DynamoDB Schema Diagram

┌─────────────────────────────────────┐
│             DynamoDB Table          │
├─────────────────────────────────────┤
│ PK: CLIENT#1234                     │
│ SK: TOKEN#ACCESS                    │
│ access_token: <string>              │
│ refresh_token: <string>             │
│ expires_at: <timestamp>             │
│ ttl: <timestamp>                    │
│ last_updated: <ISO8601>             │
└─────────────────────────────────────┘

Access Pattern Summary

ActionDynamoDB APIDescription
Fetch tokenGetItemFast read via composite key
Update tokenPutItemWrite with conditional expression
Expire old tokensTTLAuto-purge expired items

Performance & Pricing

MetricSecrets ManagerSSM StoreDynamoDB (On-Demand)
Read Latency~100ms~60ms3–5ms (with DAX)
Write Latency~120ms~80ms5–10ms
Cost~$52/moFree (throttled)$10–15/mo
ScaleModerateLimited (40 TPS)Virtually infinite

Node.js Example: Accessing Credentials in DynamoDB

Here’s a minimal Node.js example using the AWS SDK v3:

import { DynamoDBClient, GetItemCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";

const client = new DynamoDBClient({ region: "us-east-1" });
const TABLE_NAME = "client-credentials";

export async function getCredential(clientId, tokenType = "ACCESS") {
  const params = {
    TableName: TABLE_NAME,
    Key: marshall({ PK: `CLIENT#${clientId}`, SK: `TOKEN#${tokenType}` })
  };

  const response = await client.send(new GetItemCommand(params));
  return response.Item ? unmarshall(response.Item) : null;
}

export async function updateCredential(clientId, tokenType, data) {
  const item = {
    PK: `CLIENT#${clientId}`,
    SK: `TOKEN#${tokenType}`,
    ...data
  };

  const params = {
    TableName: TABLE_NAME,
    Item: marshall(item),
    ConditionExpression: "attribute_not_exists(PK) OR expires_at < :now",
    ExpressionAttributeValues: marshall({ ":now": Date.now() / 1000 })
  };

  await client.send(new PutItemCommand(params));
  return item;
}

S3 as an Alternative: When It Makes Sense

For static credentials that rarely change, Amazon S3 works surprisingly well.

Pros:

  • Versioning = audit trail
  • Dirt-cheap ($0.023/GB/month)
  • Excellent for bulk or infrequent updates

Cons:

  • Eventual consistency for overwrite operations
  • Slower for real-time token reads

Use S3 for static keys or configuration data, not high-frequency token reads.

Hybrid Architecture for the Win

           ┌─────────────────────┐
           │  EventBridge (cron) │
           └──────────┬──────────┘

             ┌────────▼────────┐
             │  ECS Tasks      │
             │  (Token Updater)│
             └────────┬────────┘

             ┌────────▼────────┐
             │  DynamoDB Table
             └────────┬────────┘

           ┌──────────▼──────────┐
           │ Lambda / API Layer  │
           │ (Reads Cached Token)│
           └─────────────────────┘
  • EventBridge triggers periodic token updates
  • ECS Tasks / Lambdas refresh tokens asynchronously
  • DynamoDB serves as the authoritative store
  • ElastiCache / Lambda extension caches hot credentials locally

Cost Comparison Summary

ServiceMonthly Cost (130 Clients)Notes
Secrets Manager~$52$0.40/secret/month
SSM Parameter Store$0Throttling at 40 TPS
DynamoDB (On-Demand)$10–15Millions of reads/writes
S3 (Optional Archive)< $1Long-term storage only

Key Takeaways & Best Practices

  • Always encrypt at rest with AWS KMS
  • Use DynamoDB TTL for token expiration
  • Cache credentials in-memory to reduce read load
  • Implement exponential backoff for retries
  • Use CloudWatch alarms for throttling and write failures
  • Test load before production deployment
  • For relational metadata, consider Aurora Serverless v2

Summary

Credential storage isn’t just a security problem — it’s a scalability and cost optimization challenge.

  • Secrets Manager is great for low-frequency, sensitive secrets.
  • Parameter Store works for small workloads but throttles at scale.
  • DynamoDB strikes the right balance: fast, cheap, scalable, and secure.

When your workloads scale, your secret store should too — and that’s where DynamoDB often wins.

More articles

S3 Incomplete Multipart Uploads Are Dangerous

A real-world story of how invisible data led to unexpected AWS costs — and how you can prevent it.

Read more

How to Stop Costly CloudFront Traffic Spikes: Protect Your AWS Project from Denial-of-Wallet Attacks

A recent Reddit thread described a simple yet painful story: A small project suddenly received 130 million requests on its CloudFront distribution — resulting in a $195 bill in just a few days. The intent wasn’t to take the service offline; it was to drain the wallet.

Read more

Tell us about your project

Our office

  • 425, Avadh Kontina
    Vip Road, Canal Road Corner, near CB patel club
    Surat, Gujarat 395007
    Google Map