mongodb/node-mongodb-native · error · MongoAPIError

Cannot use $out or $merge stage with ITERATION timeoutMode

Error message

Cannot use $out or $merge stage with ITERATION timeoutMode

What it means

Thrown as a MongoAPIError in the AggregationCursor constructor (and again in addStage()) when timeoutMS is set, timeoutMode resolves to CursorTimeoutMode.ITERATION, and the pipeline's last stage is $out or $merge. $out and $merge are write stages that materialize results into a collection; under ITERATION mode the deadline resets per next() call, which does not bound the actual materialization work the server performs for those stages, so the driver forbids the combination to avoid a misleading timeout guarantee. The same check is applied lazily in addStage() so a stage added after construction is also caught.

Solutions

  1. Use CursorTimeoutMode.LIFETIME (or omit timeoutMode so a non-tailable aggregation defaults to LIFETIME) so the timeoutMS budget covers the whole $out/$merge operation.
  2. If you need per-iteration semantics, remove the $out/$merge stage and materialize results client-side.
  3. Set timeoutMS at the MongoClient/command level with LIFETIME semantics for materialization jobs instead of ITERATION on the cursor.

Example fix

// before: ITERATION mode + $out is rejected
const cursor = collection.aggregate(
  [{ $match: { active: true } }, { $out: 'active_users' }],
  { timeoutMS: 5000, timeoutMode: 'iteration' }
);

// after: use LIFETIME (default for non-tailable) so the budget covers the write
const cursor = collection.aggregate(
  [{ $match: { active: true } }, { $out: 'active_users' }],
  { timeoutMS: 5000 } // timeoutMode defaults to 'cursorLifetime'
);
Defensive patterns

Strategy: validation

Validate before calling

function validateAggregationTimeout(pipeline: Document[], opts: AggregateOptions) {
  const last = pipeline[pipeline.length - 1];
  const writesToCollection = last?.$out != null || last?.$merge != null;
  if (opts.timeoutMS != null && opts.timeoutMode === 'iteration' && writesToCollection) {
    throw new RangeError('Use cursorLifetime (default) timeoutMode with $out/$merge, or remove timeoutMode.');
  }
}

Prevention

When it happens

Trigger: db.collection.aggregate([{ $match: ... }, { $out: 'results' }], { timeoutMS: 5000 }) where the cursor defaults to ITERATION mode is fine only because $out cursors return immediately; the error specifically fires when timeoutMode is ITERATION. Concretely: options with timeoutMS + explicit timeoutMode: 'iteration' + a pipeline ending in $out/$merge, or building the cursor then calling .out(...) / .addStage({ $merge }) under those options.

Common situations: Adopting CSOT (timeoutMS) on an existing ETL aggregation that uses $merge/$out; copying ITERATION-mode options from a reporting aggregation into a materialization aggregation; chaining .out() on a cursor created with timeoutMS and explicit ITERATION mode.

Understand the failure class

Related errors


AI-assisted analysis of mongodb/node-mongodb-native@dce7939f86 (2026-08-11). Data as JSON: /api/errors/edf80cdfbadc4f81. Report an issue: GitHub.

Appendix: source

Thrown at src/cursor/aggregation_cursor.ts:57

  constructor(
    client: MongoClient,
    namespace: MongoDBNamespace,
    pipeline: Document[] = [],
    options: AggregateOptions & Abortable = {}
  ) {
    super(client, namespace, options);

    this.pipeline = pipeline;
    this.aggregateOptions = options;

    const lastStage: Document | undefined = this.pipeline[this.pipeline.length - 1];

    if (
      this.cursorOptions.timeoutMS != null &&
      this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION &&
      (lastStage?.$merge != null || lastStage?.$out != null)
    )
      throw new MongoAPIError('Cannot use $out or $merge stage with ITERATION timeoutMode');
  }

  clone(): AggregationCursor<TSchema> {
    const clonedOptions = mergeOptions({}, this.aggregateOptions);
    delete clonedOptions.session;
    return new AggregationCursor(this.client, this.namespace, this.pipeline, {
      ...clonedOptions
    });
  }

  override map<T>(transform: (doc: TSchema) => T): AggregationCursor<T> {
    return super.map(transform) as AggregationCursor<T>;
  }

  /** @internal */
  async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
    const options = {
      ...this.aggregateOptions,

View on GitHub (pinned to dce7939f86)