> For the complete documentation index, see [llms.txt](https://atomic-blend.gitbook.io/mongo2pg/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://atomic-blend.gitbook.io/mongo2pg/operating-a-migration/operations/operational-realities.md).

# Operational realities

These are the things that cost real debugging time to discover, because none of them are visible from reading the mapping file or the CLI `--help` text. Read this page before running mongo2pg against production.

## `maxLagSeconds` has a physical floor

Replication lag is measured against the change stream's **position**, and on a quiet database that position only advances when the MongoDB server writes a **periodic no-op** — controlled by `periodicNoopIntervalSecs`, which defaults to **10 seconds**. A `cutover.maxLagSeconds` at or below that interval asks for a lag figure that can never be satisfied on an idle collection, so the cutover gate would simply never open.

Config validation enforces this directly: a value ≤ 10 is **rejected outright**, a value below 25 **warns**, and the default is **30** (3× the no-op interval — comfortably above the floor with margin for jitter). If you must lower `periodicNoopIntervalSecs` itself on the MongoDB server, `maxLagSeconds` has to come down with it; the two are coupled by the mechanism, not by coincidence.

## A wedged stream is invisible to lag

A change stream can be **connected, not erroring, and delivering nothing** — a wedged cursor — while the lag metric stays low, because a resume token is a high-water mark: it legitimately advances whenever the stream reopens and finds nothing new to deliver, even if the cursor itself has stopped completing round trips. No lag threshold can distinguish that from healthy idle replication.

`mongo2pg_stream_alive` is the metric that catches it, and it is **not** the same signal as lag:

```promql
mongo2pg_stream_alive{service_name="productivity"} == 0
```

This is exactly what the `Mongo2pgStreamDead` alert (severity `critical`, `for: 5m`) fires on. Two details make the metric work as designed and must not be "improved" away:

* It is a `GaugeVec` with **no variable labels**, so the series is **absent**, not `0`, for the entire duration of backfill — before any cursor has opened at all. `== 0` cannot match an absent series, so the alert stays silent through a normal backfill and only fires once a cursor that was genuinely open has stopped answering.
* Rewriting the alert as `!= 1`, wrapping it in `absent_over_time(...)`, `or vector(0)`, or a clamp would make it fire on every single migration during backfill — which trains operators to ignore the one signal that reports a silently dead cursor. Alert on `stream_alive == 0` and nothing else.

If lag looks fine but a migration has visibly stalled — quarantine flat, `snapshot progress` lines gone quiet, phase stuck — check `stream_alive` before anything else.

## Foreign keys are created after backfill, not before

`ADD CONSTRAINT … NOT VALID` skips only the initial scan of rows that **predate** the constraint; PostgreSQL still enforces it on every insert and update from the moment it exists. A constraint present *while* backfill is still running would therefore reject any child row whose parent has not landed yet — which happens routinely, since collections backfill independently and a self-reference (`tasks.parent_task_id`) has no valid topological order at all.

mongo2pg creates every FK constraint only **after every collection has finished backfilling**, `NOT VALID`, and validates them as one of the conditions `cutover check` gates on. See [Why constraints are created after backfill](/mongo2pg/concepts/concepts/foreign-keys.md#why-constraints-are-created-after-backfill) for the full mechanism. The practical consequence: a dangling reference that already existed in the *source* MongoDB data does not fail the backfill — it surfaces later, at the cutover gate, as a named `VALIDATE CONSTRAINT` failure, which is a far more actionable place to discover it than a backfill that silently refused rows for hours.

## Quarantined rows block cutover by design

This is covered fully in [Quarantine](/mongo2pg/concepts/concepts/quarantine.md) and [Handling quarantine](/mongo2pg/operating-a-migration/operations/handling-quarantine.md), but the operational summary: the fix is always **correct the document in MongoDB, then `mongo2pg quarantine retry`** — which re-reads the *current* version of the document. Retrying without fixing anything just reproduces the same failure.

## Exit codes are a contract

An operator's deploy script branches on the exit code, not on parsing log output, so a wrong code is a silent production incident waiting to happen:

| Code | Meaning                 |
| ---- | ----------------------- |
| `0`  | ok                      |
| `1`  | runtime error           |
| `2`  | usage / config error    |
| `3`  | destructive DDL refused |
| `4`  | cutover not ready       |

See the full table in the [CLI reference](/mongo2pg/reference/cli-reference.md#exit-codes). In particular, `cutover check` exiting non-zero (`4`) must never be treated the same as `1` by a deploy pipeline — `4` means "ran successfully and the honest answer is not yet," which is a completely different situation from a broken connection.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://atomic-blend.gitbook.io/mongo2pg/operating-a-migration/operations/operational-realities.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
