> 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/verification-and-checks.md).

# Verification and checks

`mongo2pg verify` runs independent checks per collection; `mongo2pg cutover check` re-runs the ones `cutover.requireVerify` names, on live data, every time — a previously recorded pass is never accepted as current, because the whole point of the gate is to describe the data as it is at the moment an operator is about to switch production over.

## The three verification kinds

| Check        | Method                                                                                                                                                                         | Catches                                                                                                                                                                   | Cost                            |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| **count**    | `countDocuments` vs `SELECT count(*)`, both filtered by the collection's soft-delete rule                                                                                      | A gross row-count mismatch — missing or extra rows                                                                                                                        | Cheap; safe to run continuously |
| **checksum** | `_id`-range buckets (8 by default): both sides hash `(primary key, updated_at)` pairs per bucket and compare                                                                   | A row whose *contents* drifted without the count changing — and localizes the mismatch to a bucket's `[low_id, high_id]` range, not just "something is wrong"             | Medium; run periodically        |
| **sample**   | N random documents (100 by default), **re-mapped through the live `Mapper`** — the same one the pipeline writes through — and diffed field by field against the PostgreSQL row | *Mapping* bugs, not just transport bugs — a wrong coercion or a mis-relinked FK shows up as a concrete field diff instead of being invisible to a raw-document comparison | Expensive; run on demand        |

The distinction between checksum and sample is deliberate and complementary: checksum catches a PostgreSQL row **with no document behind it** (it is keyed by primary keys the MongoDB side derived, so an orphan row is invisible to it by construction) — count catches that instead. Sample is the only one of the three that re-runs the real mapping logic rather than comparing values as they already sit in each database, which is why it is the one that catches a coercion or relinking bug rather than only a transport bug.

```bash
mongo2pg verify                                        # all three checks, every collection
mongo2pg verify --kind count,checksum                   # skip the expensive one
mongo2pg verify --kind sample --sample 500               # a bigger sample, just this check
mongo2pg verify --collections tasks,notes --buckets 32   # finer checksum localization
```

## Every condition `cutover check` gates on

`mongo2pg cutover check` exits `0` only when, for **every** collection:

1. **Phase is `streaming` or better** — see [The phase machine](/mongo2pg/concepts/concepts/phases.md). This is not re-derived by the verify package; it is consumed as-is from the pipeline's own latching readiness predicate, which additionally accounts for demonstrated change-stream liveness — see [Operational realities](/mongo2pg/operating-a-migration/operations/operational-realities.md#a-wedged-stream-is-invisible-to-lag).
2. **Replication lag is under `cutover.maxLagSeconds`.**
3. **Zero unresolved rows in `_migration.quarantine`** (when `requireEmptyQuarantine: true`, the default) — see [Quarantine](/mongo2pg/concepts/concepts/quarantine.md).
4. **Zero unresolved rows in `_migration.pending_fk`** — a deferred foreign key that never landed would otherwise silently ship as `NULL` — see [Foreign-key relinking](/mongo2pg/concepts/concepts/foreign-keys.md).
5. **Every check named in `cutover.requireVerify` passes, re-run on current data** — not a cached result from an earlier `verify` run.
6. **Every deferred (`NOT VALID`) foreign key `VALIDATE`s successfully.** This is the check that proves referential integrity was genuinely verified, not merely skipped — see [Why constraints are created after backfill](/mongo2pg/concepts/concepts/foreign-keys.md#why-constraints-are-created-after-backfill). A failing validation is not treated as an error that aborts the run: it is reported as a failing condition, with PostgreSQL's own message (naming the constraint) carried through verbatim, alongside everything else that is wrong.
7. **Zero open cross-service dangling references** — strictly zero, no threshold. Every `ref.external` field's derived target must be a confirmed-present row in the owning service's own database (checked through `target.siblings`); a target that could not even be checked (the sibling is unreachable, or has not created that table yet) fails the gate exactly the same way a confirmed-missing one does. `cutover.allowDanglingCrossServiceRefs` lets an operator acknowledge known-open findings and proceed anyway — a fleet migrating one service at a time legitimately has siblings that have not caught up — without weakening any of the other six conditions.

   **This is the one condition an operator can override, and overriding it costs the exit code.** An acknowledged cutover still exits `0` — deliberately: that is what lets the switch unblock a CI job or a deploy script gating on exit status, which is the entire point of having it. But that also means `0` alone no longer distinguishes "every reference confirmed" from "some references were never confirmed and this operator said proceed anyway" — see [Exit codes are a contract](/mongo2pg/operating-a-migration/operations/operational-realities.md#exit-codes-are-a-contract) for why that is a deliberate trade-off, not an oversight, and what to watch instead if a script needs to tell the two apart: the printed report and a `WARN` log line always name the acknowledged counts, and `mongo2pg_dangling_refs_open` (see [Metrics](/mongo2pg/observability/observability/metrics.md)) keeps reporting the real, unfiltered count regardless of the switch. See [Cross-service references](/mongo2pg/concepts/concepts/cross-service-references.md) for the full mechanism and `mongo2pg dangling list`/`export`/`recheck` (see the [CLI reference](/mongo2pg/reference/cli-reference.md)) for working the open findings themselves.

Every collection is checked whether or not it is already ready, so one run reports everything wrong at once rather than only the first failure:

```
$ mongo2pg cutover check
collection=time_entries  ready=false  reason="phase is streaming, not cutover_ready"
collection=time_entries  ready=false  reason="3 unresolved quarantine rows"
collection=tasks         ready=true
collection=notes         ready=true
constraints              ready=false  reason="foreign key violation: task_occurrence_overrides_task_id_fkey"
exit 4
```

A [cross-service reference](/mongo2pg/concepts/concepts/cross-service-references.md) whose target row could not be confirmed to exist is condition 7 above — a genuine failure, not merely an informational line, since there is no authoritative FK constraint across two databases to fall back on and a silent pass would let a corrupt reference through cutover unnoticed.

## Verification is an audit trail, not a cache

Every run of `verify` (and the checks `cutover check` re-runs) is recorded in `_migration.verify_runs` — collection, kind, the Mongo-side and PostgreSQL-side values, whether it passed, and when. This is history for an operator to consult; it is never consulted *by* `cutover check` as a substitute for re-running the check live.


---

# 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/verification-and-checks.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.
