Prerequisites
Mongoose version
9.2.4 (also confirmed on 9.3.3)
Node.js version
v24.13.0
MongoDB server version
7.0 (mongodb-memory-server)
Description
connection.startSession() hangs and times out with Connection operation buffering timed out when test frameworks' fake timers are active (e.g. Vitest's vi.useFakeTimers({ toFake: ['Date'] })).
The Date snapshot fix from #14949 (const Date = globalThis.Date at the top of connection.js) correctly protects the readyState getter — it always uses the real Date.now(). However, the heartbeat handler in drivers/node-mongodb-native/connection.js was not updated and still uses the global Date.now():
// drivers/node-mongodb-native/connection.js:481
client.on('serverHeartbeatSucceeded', () => {
conn._lastHeartbeatAt = Date.now(); // ← uses faked Date!
});
This creates a mismatch:
| Component |
Date.now() source |
Value with fake timers |
readyState getter (connection.js) |
Snapshotted (real) |
e.g. 2026-03-27 |
Heartbeat handler (native/connection.js) |
Global (faked) |
e.g. 2025-01-01 |
The staleness check Date.now() - _lastHeartbeatAt >= heartbeatFrequencyMS * 2 computes realTime - fakedTime, which easily exceeds the 20s threshold → readyState returns disconnected even though the connection is healthy → startSession() buffers → hangs → times out.
Steps to Reproduce
Save as repro.mjs:
import mongoose from "mongoose";
import { MongoMemoryReplSet } from "mongodb-memory-server";
const BUFFER_TIMEOUT_MS = 5_000;
async function main() {
const replSet = await MongoMemoryReplSet.create({ replSet: { count: 1 } });
const conn = mongoose.connection;
await mongoose.connect(replSet.getUri(), { bufferTimeoutMS: BUFFER_TIMEOUT_MS });
console.log("Connected. readyState:", conn.readyState); // 1
// Wait for a heartbeat
if (conn._lastHeartbeatAt == null) {
await new Promise((r) => {
conn.client.on("serverHeartbeatSucceeded", function onHB() {
conn.client.removeListener("serverHeartbeatSucceeded", onHB);
r();
});
});
}
// Confirm startSession works
const ok = await conn.startSession();
console.log("startSession() works before faking ✓");
await ok.endSession();
// Simulate what vi.useFakeTimers({ toFake: ['Date'] }) does
const RealDate = globalThis.Date;
const FAKE_TIME = new RealDate("2020-01-01T00:00:00Z").getTime();
globalThis.Date = class FakeDate extends RealDate {
constructor(...args) {
if (args.length === 0) return new RealDate(FAKE_TIME);
return new RealDate(...args);
}
static now() { return FAKE_TIME; }
};
// Wait for heartbeat to record _lastHeartbeatAt with faked Date.now()
await new Promise((r) => {
conn.client.on("serverHeartbeatSucceeded", function onHB() {
conn.client.removeListener("serverHeartbeatSucceeded", onHB);
r();
});
});
console.log("_readyState:", conn._readyState, "(1 = connected)");
console.log("readyState: ", conn.readyState, "(0 = falsely disconnected!)");
console.log("_lastHeartbeatAt:", conn._lastHeartbeatAt, "(faked time)");
console.log("Real Date.now(): ", RealDate.now());
// startSession() hangs and times out
try {
await conn.startSession();
} catch (err) {
console.log("startSession() failed:", err.message);
}
// Prove connection was healthy
globalThis.Date = RealDate;
conn._lastHeartbeatAt = null;
const recovered = await conn.startSession();
console.log("After fix → startSession() works ✓");
await recovered.endSession();
await mongoose.disconnect();
await replSet.stop();
}
main().catch(console.error);
Run: node repro.mjs
Output
Connected. readyState: 1
startSession() works before faking ✓
_readyState: 1 (1 = connected)
readyState: 0 (0 = falsely disconnected!)
_lastHeartbeatAt: 1577836800000 (faked time)
Real Date.now(): 1774602664507
startSession() failed: Connection operation buffering timed out after 5000ms
After fix → startSession() works ✓
Expected Behavior
startSession() should work regardless of whether globalThis.Date has been replaced by fake timers, since the readyState getter already uses a snapshotted Date.
Suggested Fix
Add the same Date snapshot to drivers/node-mongodb-native/connection.js so the heartbeat handler also uses the real Date.now():
// At the top of drivers/node-mongodb-native/connection.js
const Date = globalThis.Date;
This ensures both the readyState getter and the heartbeat handler use the same (real) Date, making the staleness check consistent regardless of fake timers.
Prerequisites
Mongoose version
9.2.4 (also confirmed on 9.3.3)
Node.js version
v24.13.0
MongoDB server version
7.0 (mongodb-memory-server)
Description
connection.startSession()hangs and times out withConnection operation buffering timed outwhen test frameworks' fake timers are active (e.g. Vitest'svi.useFakeTimers({ toFake: ['Date'] })).The Date snapshot fix from #14949 (
const Date = globalThis.Dateat the top ofconnection.js) correctly protects thereadyStategetter — it always uses the realDate.now(). However, the heartbeat handler indrivers/node-mongodb-native/connection.jswas not updated and still uses the globalDate.now():This creates a mismatch:
Date.now()sourcereadyStategetter (connection.js)native/connection.js)The staleness check
Date.now() - _lastHeartbeatAt >= heartbeatFrequencyMS * 2computesrealTime - fakedTime, which easily exceeds the 20s threshold →readyStatereturnsdisconnectedeven though the connection is healthy →startSession()buffers → hangs → times out.Steps to Reproduce
Save as
repro.mjs:Run:
node repro.mjsOutput
Expected Behavior
startSession()should work regardless of whetherglobalThis.Datehas been replaced by fake timers, since thereadyStategetter already uses a snapshottedDate.Suggested Fix
Add the same
Datesnapshot todrivers/node-mongodb-native/connection.jsso the heartbeat handler also uses the realDate.now():This ensures both the
readyStategetter and the heartbeat handler use the same (real)Date, making the staleness check consistent regardless of fake timers.