{"record":{"id":"edf80cdfbadc4f81","repo":"mongodb/node-mongodb-native","slug":"cannot-use-out-or-merge-stage-with-iteration-tim","errorCode":null,"errorMessage":"Cannot use $out or $merge stage with ITERATION timeoutMode","messagePattern":"Cannot use \\$out or \\$merge stage with ITERATION timeoutMode","errorType":"exception","errorClass":"MongoAPIError","httpStatus":null,"severity":"error","filePath":"src/cursor/aggregation_cursor.ts","lineNumber":57,"sourceCode":"  constructor(\n    client: MongoClient,\n    namespace: MongoDBNamespace,\n    pipeline: Document[] = [],\n    options: AggregateOptions & Abortable = {}\n  ) {\n    super(client, namespace, options);\n\n    this.pipeline = pipeline;\n    this.aggregateOptions = options;\n\n    const lastStage: Document | undefined = this.pipeline[this.pipeline.length - 1];\n\n    if (\n      this.cursorOptions.timeoutMS != null &&\n      this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION &&\n      (lastStage?.$merge != null || lastStage?.$out != null)\n    )\n      throw new MongoAPIError('Cannot use $out or $merge stage with ITERATION timeoutMode');\n  }\n\n  clone(): AggregationCursor<TSchema> {\n    const clonedOptions = mergeOptions({}, this.aggregateOptions);\n    delete clonedOptions.session;\n    return new AggregationCursor(this.client, this.namespace, this.pipeline, {\n      ...clonedOptions\n    });\n  }\n\n  override map<T>(transform: (doc: TSchema) => T): AggregationCursor<T> {\n    return super.map(transform) as AggregationCursor<T>;\n  }\n\n  /** @internal */\n  async _initialize(session: ClientSession): Promise<InitialCursorResponse> {\n    const options = {\n      ...this.aggregateOptions,","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/dce7939f86fb283e167ad709955abedb7bf23124/src/cursor/aggregation_cursor.ts#L39-L75","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use CursorTimeoutMode.LIFETIME (or omit timeoutMode so a non-tailable aggregation defaults to LIFETIME) so the timeoutMS budget covers the whole $out/$merge operation.","If you need per-iteration semantics, remove the $out/$merge stage and materialize results client-side.","Set timeoutMS at the MongoClient/command level with LIFETIME semantics for materialization jobs instead of ITERATION on the cursor."],"exampleFix":"// before: ITERATION mode + $out is rejected\nconst cursor = collection.aggregate(\n  [{ $match: { active: true } }, { $out: 'active_users' }],\n  { timeoutMS: 5000, timeoutMode: 'iteration' }\n);\n\n// after: use LIFETIME (default for non-tailable) so the budget covers the write\nconst cursor = collection.aggregate(\n  [{ $match: { active: true } }, { $out: 'active_users' }],\n  { timeoutMS: 5000 } // timeoutMode defaults to 'cursorLifetime'\n);","handlingStrategy":"validation","validationCode":"function validateAggregationTimeout(pipeline: Document[], opts: AggregateOptions) {\n  const last = pipeline[pipeline.length - 1];\n  const writesToCollection = last?.$out != null || last?.$merge != null;\n  if (opts.timeoutMS != null && opts.timeoutMode === 'iteration' && writesToCollection) {\n    throw new RangeError('Use cursorLifetime (default) timeoutMode with $out/$merge, or remove timeoutMode.');\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["For $out/$merge aggregations, omit timeoutMode so it defaults to cursorLifetime under timeoutMS.","Run a pipeline-shape validator that flags write stages combined with ITERATION mode.","Keep ETL/materialization aggregations on a separate options path from reporting aggregations."],"tags":["aggregation","cursor","csot","timeout-mode","out-merge","invalid-argument"],"backgroundTag":null,"analyzedSha":"dce7939f86fb283e167ad709955abedb7bf23124","analyzedAt":"2026-08-11T04:54:53.215Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}