Scaling DocumentDB: From Prototype to Production
Amazon DocumentDB is a fully managed, MongoDB-compatible database service designed to handle large-scale, JSON-like document workloads. When you build a prototype, a single instance with default settings is usually enough. But moving to production introduces traffic spikes, larger datasets, stricter latency requirements, and higher availability expectations. This tutorial walks you through the journey of scaling DocumentDB from a small prototype to a robust production system.
What Is DocumentDB Scaling?
DocumentDB scaling refers to the set of strategies and configurations used to grow your database's capacity as workload demands increase. Scaling in DocumentDB happens along two main axes:
- Vertical scaling: Increasing the compute and memory capacity of your instance by changing the instance class (for example, moving from
db.r5.largetodb.r5.4xlarge). - Horizontal scaling: Adding read replicas to distribute read traffic and increasing the number of shards to distribute storage and compute across multiple nodes.
DocumentDB also separates storage from compute. Storage automatically grows in increments of 10GB up to 64TB per cluster, so you rarely need to manually provision disk space. Compute, however, must be planned and scaled deliberately.
Why Scaling Matters
A prototype might serve a few hundred requests per minute from a single instance. In production, the same application may need to handle thousands of concurrent connections, complex aggregation pipelines, and strict service-level objectives for latency and uptime. Without proper scaling, you risk connection exhaustion, CPU bottlenecks, slow queries, and outages during failover events.
Scaling also affects cost. Over-provisioning wastes money, while under-provisioning degrades user experience. A well-planned scaling strategy balances performance, availability, and cost.
Understanding DocumentDB Architecture
Before scaling, it helps to understand the components of a DocumentDB cluster:
- Primary instance: Handles all writes and serves as the source of truth for replicated data.
- Replicas: Read-only copies of the primary. DocumentDB supports up to 15 replicas per cluster.
- Storage volume: A distributed, shared storage layer that all instances in the cluster access. It grows automatically.
- Cluster endpoint: A single DNS endpoint that applications use to connect. The writer endpoint always points to the primary, while reader endpoints distribute traffic across replicas.
This architecture means that reads and writes can be scaled independently. Writes are limited by the capacity of the primary instance, while reads can be distributed across replicas.
Step 1: Connecting With the Right Driver
Production applications should use the MongoDB Node.js driver or another supported driver configured for DocumentDB. Always use TLS and connect through the cluster endpoint rather than individual instance endpoints.
const { MongoClient } = require('mongodb');
const uri = 'mongodb://<user>:<password>@<cluster-endpoint>:27017/?tls=true&replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false';
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 50,
minPoolSize: 5,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
socketTimeoutMS: 30000
});
async function connect() {
await client.connect();
console.log('Connected to DocumentDB');
return client.db('mydb');
}
module.exports = { connect, client };
Notice the readPreference=secondaryPreferred parameter. This directs read operations to replicas, reducing load on the primary. Writes always go to the primary regardless of this setting.
Step 2: Vertical Scaling for Immediate Headroom
The fastest way to gain performance is to upgrade the instance class. DocumentDB offers memory-optimized r5 and r6g instance classes, which are well-suited for document workloads because they provide high memory-to-CPU ratios.
You can modify the instance class using the AWS CLI:
aws docdb modify-db-instance \
--db-instance-identifier my-docdb-instance \
--db-instance-class db.r5.4xlarge \
--apply-immediately
For production systems with strict uptime requirements, omit --apply-immediately so the change happens during the next maintenance window. DocumentDB will perform a failover to a replica, promote it, and apply the change with minimal downtime.
Choose instance classes based on your workload profile:
- Write-heavy workloads: Larger instances with more CPU to handle indexing and write throughput.
- Read-heavy workloads: More replicas on smaller instances, since reads scale horizontally.
- Memory-intensive aggregations: Memory-optimized instances to keep working sets in RAM.
Step 3: Adding Read Replicas
Read replicas are the primary mechanism for scaling read throughput. Each replica has its own compute resources but shares the same storage volume, so replication lag is typically minimal.
aws docdb create-db-instance \
--db-instance-identifier my-docdb-replica-1 \
--db-instance-class db.r5.xlarge \
--engine docdb \
--db-cluster-identifier my-docdb-cluster \
--promotion-tier 1
The --promotion-tier parameter controls failover priority. Instances in tier 0 are promoted first during a failover. Distribute your replicas across tiers to control which instance becomes primary if the current one fails.
To use replicas effectively, configure your application to route reads through the reader endpoint:
const readerUri = 'mongodb://<user>:<password>@<reader-endpoint>:27017/?tls=true&replicaSet=rs0&readPreference=secondaryPreferred';
const readClient = new MongoClient(readerUri, {
maxPoolSize: 100,
minPoolSize: 10
});
For write operations, use the writer endpoint. This separation ensures that read traffic never competes with writes for resources on the primary.
Step 4: Sharding for Horizontal Write Scaling
Vertical scaling and replicas have limits. When your write throughput exceeds what a single primary can handle, or when your dataset grows so large that indexes no longer fit in memory, you need sharding. DocumentDB supports horizontal scaling through sharded clusters, which distribute data across multiple shards based on a shard key.
To enable sharding, you first connect to the cluster and enable the feature:
const adminDb = client.db('admin');
async function enableSharding() {
await adminDb.command({ enableSharding: 'mydb' });
console.log('Sharding enabled for mydb');
}
async function shardCollection(collectionName, shardKey) {
await adminDb.command({
shardCollection: `mydb.${collectionName}`,
key: shardKey
});
console.log(`Collection ${collectionName} sharded`);
}
// Example: shard the orders collection by user_id
enableSharding()
.then(() => shardCollection('orders', { user_id: 1 }))
.catch(console.error);
Choosing a shard key is the most important decision in a sharded architecture. A good shard key has high cardinality, distributes writes evenly, and supports common query patterns. Poor shard key choices lead to hot shards, where one shard receives most of the traffic.
- High cardinality: The key should have many distinct values to allow even distribution.
- Low frequency: No single value should dominate, or that shard becomes a hotspot.
- Monotonic change avoidance: Avoid keys like timestamps that always increase, since new writes cluster on a single shard.
A common pattern is to use a hashed shard key on a field with high cardinality:
await adminDb.command({
shardCollection: 'mydb.events',
key: { event_id: 'hashed' }
});
Step 5: Indexing for Query Performance
Scaling is not just about adding hardware. Efficient queries reduce the load on every node. Indexes are the single most impactful performance lever in DocumentDB.
Without an index, DocumentDB performs a collection scan, examining every document. On a collection with millions of documents, this destroys performance. Always index fields used in filter, sort, and join operations.
const db = await connect();
const orders = db.collection('orders');
// Single field index
await orders.createIndex({ user_id: 1 });
// Compound index for common query patterns
await orders.createIndex({ user_id: 1, created_at: -1 });
// Text index for search
await orders.createIndex({ description: 'text' });
// TTL index for automatic document expiration
await orders.createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 });
Use the explain() method to verify that queries use indexes:
const explanation = await orders
.find({ user_id: 'u123' })
.sort({ created_at: -1 })
.explain();
console.log(JSON.stringify(explanation, null, 2));
Look for IXSCAN in the output, which indicates an index was used. If you see COLLSCAN, the query is doing a full collection scan and needs an index.
Step 6: Connection Management
Connection management is critical at scale. Each connection consumes memory on the DocumentDB instance. Too many connections can exhaust resources and cause failures.
DocumentDB has a default connection limit that varies by instance class. For example, an r5.large instance supports approximately 1,000 connections, while an r5.8xlarge supports around 8,000. Monitor the DatabaseConnections CloudWatch metric to track usage.
Best practices for connection management include:
- Use a connection pool with a sensible maximum size. A pool of 50 to 100 connections per application instance is often sufficient.
- Reuse the same
MongoClientinstance across requests. Do not create a new client for every operation. - Place a connection pooler or proxy in front of DocumentDB if you have many serverless functions that each open connections. Amazon RDS Proxy does not support DocumentDB, but you can use a sidecar proxy like
mongosor a custom pooling layer. - Set idle timeout values to reclaim unused connections.
const client = new MongoClient(uri, {
maxPoolSize: 50,
minPoolSize: 5,
maxIdleTimeMS: 30000,
waitQueueTimeoutMS: 5000
});
Step 7: Monitoring and Alerting
You cannot scale what you cannot measure. DocumentDB integrates with Amazon CloudWatch, which exposes dozens of metrics. The most important metrics for scaling decisions are:
- CPUUtilization: Sustained high CPU indicates the need for a larger instance or more replicas.
- FreeableMemory: Low freeable memory suggests the working set no longer fits in RAM.
- DatabaseConnections: Approaching the limit means you need connection pooling or a larger instance.
- DatabaseCursors: High cursor counts can indicate queries that return too many results without pagination.
- WriteLatency and ReadLatency: Increasing latency signals storage or compute pressure.
- ReplicaLag: High lag means replicas are falling behind the primary, which can cause stale reads.
Set up CloudWatch alarms for these metrics:
aws cloudwatch put-metric-alarm \
--alarm-name "DocDB-High-CPU" \
--alarm-description "Alert when CPU exceeds 80% for 5 minutes" \
--metric-name CPUUtilization \
--namespace AWS/DocDB \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--dimensions Name=DBInstanceIdentifier,Value=my-docdb-instance \
--alarm-actions "arn:aws:sns:us-east-1:123456789012:alerts"
Also enable Performance Insights and DocumentDB profiler to identify slow queries. The profiler logs operations that exceed a configurable threshold:
aws docdb modify-db-cluster-parameter-group \
--db-cluster-parameter-group-name my-param-group \
--parameters ParameterName=profiler,ParameterValue=true,ApplyMethod=immediate \
--parameters ParameterName=profiler_threshold_ms,ParameterValue=100,ApplyMethod=immediate
Step 8: High Availability and Failover
Production systems must survive instance failures. DocumentDB provides automatic failover: if the primary fails, a replica is promoted to primary, typically within 30 seconds.
To maximize availability:
- Deploy at least two replicas in different Availability Zones.
- Use the cluster writer endpoint in your application so that after failover, traffic automatically routes to the new primary without code changes.
- Configure failover priority tiers so the most capable instance becomes primary.
- Test failover regularly using the
failover-db-clustercommand.
aws docdb failover-db-cluster \
--db-cluster-identifier my-docdb-cluster \
--target-db-instance-identifier my-docdb-replica-1
Applications should also implement retry logic for transient connection errors during failover:
async function executeWithRetry(operation, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 500;
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const result = await executeWithRetry(() => {
return orders.insertOne({ user_id: 'u123', total: 99.99 });
});
Best Practices Summary
- Start with the right instance class. Memory-optimized instances are usually the best choice for document workloads.
- Index aggressively but intentionally. Every index speeds up reads but slows down writes and consumes memory. Drop unused indexes.
- Separate read and write traffic. Use the reader endpoint for read-heavy operations and the writer endpoint for writes.
- Choose shard keys carefully. A bad shard key is expensive to change later. Test with realistic data volumes before committing.
- Monitor continuously. Set alarms for CPU, memory, connections, and latency before you need them.
- Implement retry logic. Network blips and failovers happen. Your application should handle them gracefully.
- Use connection pooling. Never open a new connection per request. Reuse clients and cap pool sizes.
- Paginate large result sets. Avoid returning thousands of documents in a single query. Use
limit()and cursor-based pagination. - Test failover before production incidents. Know how your application behaves when the primary changes.
- Review slow query logs weekly. The profiler is your best friend for finding performance regressions early.
Conclusion
Scaling DocumentDB from prototype to production is a journey that touches architecture, query design, connection management, and operational monitoring. The prototype phase teaches you what your data looks like and how your application interacts with it. The production phase demands that you think about failure modes, traffic growth, and cost efficiency. By understanding DocumentDB's separation of storage and compute, leveraging read replicas and sharding, building proper indexes, managing connections carefully, and monitoring the right metrics, you can build a document database layer that performs reliably under real-world load. The key is to scale proactively rather than reactively, testing each change against realistic workloads before it matters. With the strategies and code patterns in this tutorial, you have a practical foundation for taking your DocumentDB workload from a single-instance prototype to a resilient, production-grade system.