Skip to content

startSession() / buffered operations hang #16183

@andreialecu

Description

@andreialecu

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions