Read Preferences: Scaling Reads Without Losing Consistency
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.
The Five Modes
| Mode | Target | Consistency | Use case |
|---|---|---|---|
primary | Primary only | Strong | Transactional reads, reads-after-writes |
primaryPreferred | Primary, fallback to secondary | Strong (usually) | Primary reads with HA failover |
secondary | Secondaries only | Eventual | Analytics, reporting, read scaling |
secondaryPreferred | Secondaries, fallback to primary | Eventual (usually) | General read scaling with fallback |
nearest | Lowest network latency member | Eventual | Geo-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.