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:
| Operation | TPS Limit | Impact |
|---|---|---|
| GetParameter | 40 TPS | Read throttling at scale |
| PutParameter | 3 TPS | Token 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
| Stage | Store | Strength | Limitation |
|---|---|---|---|
| 1️⃣ | Secrets Manager | Feature-rich | Expensive for frequent access |
| 2️⃣ | SSM Parameter Store | Free (Standard) | Throttled under load |
| 3️⃣ | DynamoDB | Scalable, fast | Requires 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
| Action | DynamoDB API | Description |
|---|---|---|
| Fetch token | GetItem | Fast read via composite key |
| Update token | PutItem | Write with conditional expression |
| Expire old tokens | TTL | Auto-purge expired items |
Performance & Pricing
| Metric | Secrets Manager | SSM Store | DynamoDB (On-Demand) |
|---|---|---|---|
| Read Latency | ~100ms | ~60ms | 3–5ms (with DAX) |
| Write Latency | ~120ms | ~80ms | 5–10ms |
| Cost | ~$52/mo | Free (throttled) | $10–15/mo |
| Scale | Moderate | Limited (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
| Service | Monthly Cost (130 Clients) | Notes |
|---|---|---|
| Secrets Manager | ~$52 | $0.40/secret/month |
| SSM Parameter Store | $0 | Throttling at 40 TPS |
| DynamoDB (On-Demand) | $10–15 | Millions of reads/writes |
| S3 (Optional Archive) | < $1 | Long-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.