← Back to DevBytes

Scaling DocumentDB: From Prototype to Production

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:

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:

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:

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.

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:

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:

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles