Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 096 / 100

Read-heavy vs write-heavy workloads: a step-by-step guide

Tune MongoDB for your traffic.

Expona Admin8 min read

Read-heavy vs write-heavy workloads: a step-by-step guide

Most MongoDB deployments don't behave the same way. A real-time inventory system handles thousands of writes per second, while an analytics dashboard might read the same data millions of times with minimal changes. Getting the balance right between read-heavy and write-heavy workloads determines whether your database runs efficiently or drowns under load. This guide walks you through tuning MongoDB for your specific traffic pattern, measuring where time is spent, and making infrastructure decisions that won't haunt you in production.

Understanding your traffic pattern first

Before you optimize anything, you need baseline numbers. Too many teams guess at their workload and build the wrong solution. Pull metrics from your monitoring stack—APM tools, MongoDB Atlas charts, or CloudWatch—and answer three questions: What percentage of operations are reads versus writes? How are those operations distributed across collections? What's your acceptable latency threshold for each?

A typical e-commerce checkout flow might be 80% reads (product lookups, inventory checks, user history) and 20% writes (order creation, payment records). An event-streaming pipeline doing ride-hailing GPS updates could flip to 5% reads and 95% writes. Analytics workloads often show read patterns where the same aggregation pipeline runs hourly against static or slowly-changing data.

Document these numbers explicitly. Share them with your team. They become your North Star for all tuning decisions ahead.

Read-heavy workloads: indexes, projections, caching and secondary reads

When most of your traffic is reads, your goal is simple: find data as fast as possible without touching unnecessary memory or disk.

Indexing strategy

Build indexes on fields you filter or sort by. For a product catalog where queries are db.products.find({category, price: {$lt: 100}}), create a compound index on {category: 1, price: 1}. MongoDB can use this index to satisfy both conditions efficiently.

javascript
db.products.createIndex({category: 1, price: 1});

Use the explain() plan to verify indexes are working. A COLLSCAN on a read-heavy workload is a red flag—it means MongoDB scanned every document instead of using an index.

javascript
db.products.find({category: "electronics", price: {$lt: 100}}).explain("executionStats");

Watch for the pitfall of over-indexing. Each index consumes RAM and slows writes. In a read-heavy system you can afford more indexes, but be deliberate. Index only the 5-10 queries that actually run frequently in production.

Projection to reduce network overhead

When you fetch a product document but only need the name, price, and image URL, don't send the entire document across the network. Use projections to specify exactly which fields to return:

javascript
db.products.find(
  {category: "electronics"},
  {name: 1, price: 1, imageUrl: 1, _id: 0}
);

This is especially powerful at scale. Reducing a 50KB document to a 2KB projection across millions of reads cuts network traffic dramatically and improves cache hit ratios in your application layer.

Caching layer

Add Redis or Memcached in front of MongoDB for frequently-accessed data. Cache the results of expensive aggregations, popular product listings, or user profiles. Set a reasonable TTL (60 seconds, 5 minutes, or more depending on how fresh the data needs to be).

A real-world example: a travel platform fetches the same hotel listings thousands of times per day. Cache those results in Redis with a 30-minute TTL. Your MongoDB instance handles far fewer reads, and users see instant responses.

Read preference: secondary reads

MongoDB replica sets have one primary (handles all writes) and multiple secondaries (lag slightly behind). For read-heavy workloads, send non-critical reads to secondaries. This spreads load and keeps your primary focused on writes.

javascript
const client = new MongoClient(uri, {
  readPreference: "secondary",
  readPreferenceTagSet: [{role: "analytics"}]
});

const db = client.db("myapp");
const analytics = db.collection("pageViews");
const result = await analytics.find({}).toArray();

Be aware: secondaries lag the primary by milliseconds to seconds. Use secondary for analytics or non-critical reads, and keep primary for transactional data that must be current.

Write-heavy workloads: fewer indexes, bulk writes and sharding

Write-heavy systems require the opposite approach. Every index you maintain slows down writes because MongoDB must update that index for every insert or update. Your goal is to write data as fast as possible and worry less about read speed.

Minimize indexes

Only create indexes that directly support your write operations or absolutely critical queries. A ride-hailing system inserting thousands of GPS updates per second needs an index on the ride ID to find the document to update, but probably doesn't need indexes on every coordinate field.

javascript
// Minimal indexing for write-heavy workload
db.gpsUpdates.createIndex({rideId: 1});
// Don't create: {lat: 1, lng: 1}, {speed: 1}, {timestamp: 1}
// unless a read query specifically requires them

If a query is infrequent or used only in reporting (not in the critical path), skip the index. Run it once per hour on a secondary replica where latency isn't critical.

Bulk writes

Insert or update data in batches, not one document at a time. This reduces the overhead of network round-trips and lock contention.

javascript
const operations = [];
for (let i = 0; i < 10000; i++) {
  operations.push({
    insertOne: {
      document: {rideId: randomId(), lat: Math.random(), lng: Math.random(), timestamp: Date.now()}
    }
  });
}
const result = await db.collection("gpsUpdates").bulkWrite(operations);
console.log(`Inserted ${result.insertedCount} documents`);

Batch size of 1,000–10,000 documents is typical. Larger batches reduce overhead but consume more memory. Tune based on your hardware and document size.

Sharding for horizontal scaling

When a single MongoDB server can't keep up with write volume, shard your data. Choose a shard key that distributes writes evenly—never use a monotonically increasing field like timestamp, which puts all new writes on a single shard.

For the ride-hailing GPS example, use rideId as the shard key. Each ride's updates go to the same shard (good for locality), and writes are distributed across all shards (avoids hotspots).

javascript
// Enable sharding on the database
db.adminCommand({enableSharding: "rideApp"});

// Create a sharded collection with rideId as shard key
db.adminCommand({
  shardCollection: "rideApp.gpsUpdates",
  key: {rideId: 1}
});

Sharding adds operational complexity (managing chunk balancing, handling shard failures), so only shard when a single server is genuinely the bottleneck. Monitor your actual write throughput before making this decision.

Mixed workloads: separate analytics from transactional traffic

Most production systems are actually mixed: a core operational database handles transactions, and analytics queries run against the same data. These two patterns conflict. Transactional queries are fast, focused reads and writes. Analytics queries are slow, full-collection scans.

The best approach: run them on separate deployments. Use a replica set or secondary node specifically for analytics. Your application writes only to the primary (for transactional workload), and analytics tools connect to a secondary or dedicated read replica (avoiding contention with live traffic).

A concrete example: an e-commerce platform writes orders to the primary in near-real time. A business intelligence tool connects to a read-only secondary and runs nightly aggregations to compute revenue by category, top customers, and so on. The analytics queries don't interfere with order writes.

If you're using MongoDB Atlas, enable the Oplog Window to retain replication history longer, and create dedicated read replicas for analytics. If you're self-hosting, run a separate replica set node with higher CPU and RAM for analytical workloads.

Measure before you optimize

Guessing about your workload wastes time and money. Use MongoDB's built-in metrics to measure:

  • Throughput: operations per second (ops/s). Both reads and writes.
  • Latency: P50, P95, P99 response times. Separate by operation type.
  • Memory usage: working set size. How much RAM you need to keep hot data in memory.
  • Index efficiency: ratio of documents examined to documents returned. Should be close to 1.
  • Replica lag: how far behind secondaries are from the primary (for read-preference tuning).

If you're on Atlas, these appear in the Monitoring tab. For self-hosted MongoDB, use the explain() method, mongostat command-line tool, or third-party monitoring like DataDog or New Relic.

Run a realistic load test against your staging environment. If your production workload is 50,000 writes and 200,000 reads per second, simulate that in staging and watch where the bottlenecks appear. You'll make far better tuning decisions with real data.

Common mistakes

  • Over-indexing read-heavy workloads: Creating an index for every possible query path. Indexes help reads but slow writes. Prioritize the top 5-10 queries by frequency.
  • Sharding too early: Adding sharding complexity before you've exhausted vertical scaling (bigger machines). Measure first. Shard only when a single server is truly maxed out.
  • Ignoring replica lag: Reading from secondaries for transactional data without checking lag. A 5-second-old secondary might return stale order status. Use primary read preference for critical reads.
  • Bulk writes that are too large: Batching 100,000 documents at once can cause memory spikes and timeout issues. Start with 5,000–10,000 and measure.
  • Projections that aren't specific enough: Fetching 20 fields when you only need 3. Measure network traffic and fine-tune projections over time.
  • Separate databases for analytics but no data sync strategy: If your analytics database falls behind the primary, business logic breaks. Use MongoDB's replication or a CDC tool to keep them in sync.

Key takeaways

  • Measure your workload (read percentage, write percentage, latency requirements) before optimizing. Guessing costs time and money.
  • For read-heavy workloads, invest in indexes, projections, caching, and secondary reads. More indexes are acceptable because writes are rare.
  • For write-heavy workloads, minimize indexes, use bulk writes, and consider sharding if a single server can't handle the load.
  • For mixed workloads, separate transactional and analytical traffic onto different deployments (primary and secondary replica, or dedicated read replicas).
  • Use MongoDB's explain() and monitoring tools (Atlas Monitoring, mongostat, or third-party APM) to validate your tuning decisions in staging before production.
  • Replica set secondaries lag the primary by milliseconds to seconds. Use them for analytics and non-critical reads, but keep primary read preference for transactional data.
  • Sharding is operationally complex. Only shard when vertical scaling has been exhausted and you've confirmed it's the real bottleneck.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles