Skip to main content
unbound mongodb at scale

Read Preferences: Scaling Reads Without Losing Consistency

3 min read Chapter 55 of 72

Read Preferences

A MongoDB replica set has one primary and one or more secondaries. By default, all reads go to the primary. This guarantees that every read sees the latest write but creates a bottleneck: the primary handles both reads and writes while secondaries sit idle for read traffic.

Read preferences allow the driver to route reads to secondaries. This distributes read load but introduces a trade-off: secondaries may lag behind the primary, so reads from secondaries may return stale data.

Read preference routing diagram. Shows primary handling writes and default reads. Arrows from Java driver to primary (readPreference=primary), from driver to secondaries (readPreference=secondary). Shows replication lag as a time delta between primary's oplog and secondary's applied position. Marks staleness window.

The Five Modes

ModeTargetConsistencyUse case
primaryPrimary onlyStrongTransactional reads, reads-after-writes
primaryPreferredPrimary, fallback to secondaryStrong (usually)Primary reads with HA failover
secondarySecondaries onlyEventualAnalytics, reporting, read scaling
secondaryPreferredSecondaries, fallback to primaryEventual (usually)General read scaling with fallback
nearestLowest network latency memberEventualGeo-distributed, latency-sensitive
// Configure read preference at the client level
MongoClientSettings settings = MongoClientSettings.builder()
    .applyConnectionString(new ConnectionString(uri))
    .readPreference(ReadPreference.secondaryPreferred(
        30, TimeUnit.SECONDS))  // maxStalenessSeconds = 30
    .build();

MongoClient client = MongoClients.create(settings);

// Override per-collection
MongoCollection<Document> readings = database.getCollection("readings")
    .withReadPreference(ReadPreference.secondary());

// Override per-query (Java Sync Driver does not support per-find read preference
// directly, so use a collection reference with the desired read preference)
MongoCollection<Document> readingsFromSecondary = database
    .getCollection("readings")
    .withReadPreference(ReadPreference.nearest());

List<Document> results = readingsFromSecondary.find(filter).into(new ArrayList<>());

maxStalenessSeconds

Without maxStalenessSeconds, a secondary could be hours behind the primary and still receive reads. The driver estimates each secondary’s lag by comparing its last applied oplog timestamp with the primary’s last write timestamp. If a secondary’s estimated lag exceeds maxStalenessSeconds, the driver excludes it from read routing.

// Secondary reads with 30-second staleness bound
ReadPreference rp = ReadPreference.secondaryPreferred(30, TimeUnit.SECONDS);

The minimum value is 90 seconds. Below that, the driver’s staleness estimation is unreliable because the heartbeat interval is 10 seconds and the estimation has inherent lag.

When Stale Reads Break the Application

The telemetry platform has a write-then-read pattern: a sensor sends a reading, and the dashboard immediately queries for the latest reading. With readPreference: secondary, the dashboard may show the previous reading because the secondary has not yet replicated the latest write.

// Broken: write to primary, read from secondary
collection.insertOne(reading);  // goes to primary
Document latest = collection
    .withReadPreference(ReadPreference.secondary())
    .find(Filters.eq("sensorId", sensorId))
    .sort(Sorts.descending("ts"))
    .first();  // may return stale data

This is not a bug. This is the expected behavior of eventual consistency. The fix is to use readPreference: primary for queries that depend on the latest write, and readPreference: secondary for queries that tolerate staleness.