validationAction error vs warn: Performance Impact Benchmarks
Choosing between validationAction: "error" and validationAction: "warn" is usually framed as a safety decision, but it is also a measurable performance decision, and this page benchmarks exactly what each mode costs on the write path. It sits under strict vs moderate validation levels within the broader MongoDB JSON Schema Validation Architecture, and the reader outcome is a repeatable methodology plus a defensible mental model: both modes pay an identical schema-evaluation CPU tax on every single write, so the throughput gap between them is not the check itself but what happens after it — error rejects and stops, while warn persists the document and writes an extra diagnostic log line. If you expect warn to be the “cheaper” mode, the numbers below will surprise you.
Operational Mechanics and Write-Path Impact
The critical fact that shapes every benchmark result is that validationAction does not gate whether the $jsonSchema is evaluated — it only gates what MongoDB does with the verdict. When any validator is attached, the storage engine parses the incoming BSON and walks the schema keyword tree on every insert, update, replace, and findAndModify, and that traversal is the dominant CPU term in both modes. The action dial then diverges only on the failure branch:
errorrejects a failing write, returnsWriteErrorcode121, and performs no persistence for that document. The rejected write does zero storage I/O, so a workload with many violations undererrorcan actually finish faster than a clean one, because rejected documents never touch the journal or indexes.warnalways persists the document — valid or not — and, on a failing write, additionally emits a warning line to themongoddiagnostic log (componentSTORAGE, message id20294). That is a second, asynchronous write path: extra serialization, extra log I/O, and, crucially, a non-conforming document now durably stored in the collection.
So the honest comparison is not “fast validation versus slow validation.” It is “same evaluation cost, different tail.” The table below fixes the five dimensions that actually differ, evaluated per write:
| Mode | Schema-eval CPU (per write) | Persistence on failure | Extra log I/O on failure | Data correctness | Right use |
|---|---|---|---|---|---|
| Validator off | None | N/A | None | Not enforced | Baseline measurement only |
warn |
Full traversal | Document is written | One mongod log line per failure |
Non-conforming data accumulates | Time-boxed observation before enforcing |
error |
Full traversal | Write is rejected (no persist) | None | Guaranteed conforming | Steady-state production enforcement |
The single largest lever on the shared CPU term is schema shape, not the action. Deeply nested allOf chains multiply the number of subschemas evaluated per document, and an unanchored pattern regex (one without ^/$) forces the engine to attempt a match at every offset in the string, which turns a cheap field check into a super-linear scan on long values. Two collections with the same warn/error setting can differ by an order of magnitude in write cost purely because one schema uses tight, anchored constraints and the other buries a catch-all allOf of unanchored regexes. Benchmark the schema you will actually ship, on representative document sizes — a synthetic three-field schema tells you nothing about a 40-field contract with nested array validation.
Exact Diagnostic Fingerprints and Fast Resolution
When a benchmark or a production dashboard shows an unexpected write-latency profile, these are the signatures that tell you which mode is really active and where the cost is going:
| Signature | Root cause | Resolution |
|---|---|---|
| P95 insert latency rises the moment the validator is attached, before any failures | Shared schema-eval CPU tax is now on every write | Expected; tune schema shape, not the action |
mongod log fills with id 20294 warning lines under load |
warn is active and a large fraction of writes violate the schema |
Remediate the source, then promote to error per the rollout below |
| Collection document count keeps rising but conforming-count does not | warn is silently persisting non-conforming documents |
Switch to error once drift is remediated; warn is not a steady state |
Rejected-write throughput exceeds clean-write throughput under error |
Rejected writes skip persistence and index maintenance | Expected artifact; measure clean-path P95 separately |
Confirm which action is live before trusting any number — the collection catalog is ground truth, not the deploy script:
from pymongo import MongoClient
db = MongoClient("mongodb://primary:27017").bench_db
opts = db.command("listCollections", filter={"name": "orders"})["cursor"]["firstBatch"][0]["options"]
print(opts.get("validationLevel"), opts.get("validationAction"))
# => strict warn (or None None if no validator is attached)
Step-by-Step Playbook
To produce numbers you can defend, measure three configurations — validator off, warn, and error — against the same document generator on the same hardware, and report throughput and P95 latency rather than a single mean. Treat the figures the harness prints as illustrative of your environment only; publish your own measured numbers, never borrowed ones.
1. Fix the workload. Generate a deterministic stream of documents with a known valid/invalid mix so every run is comparable. Keep the shape identical to production, including the field that will violate the schema:
import random
def make_doc(i: int, invalid_rate: float = 0.1) -> dict:
bad = random.random() < invalid_rate
return {
"tenant_id": (12345 if bad else "acme"), # int is invalid; str is valid
"status": ("open" if not bad else "??"),
"amount": round(random.uniform(1, 5000), 2),
"created_at": i,
}
2. Define the three configurations. Attach the same $jsonSchema for the warn and error runs, and detach it entirely for the baseline. Anchor every pattern so the regex cost is honest:
SCHEMA = {
"bsonType": "object",
"required": ["tenant_id", "status", "amount"],
"properties": {
"tenant_id": {"bsonType": "string", "pattern": "^[a-z0-9_]{2,32}$"},
"status": {"bsonType": "string", "enum": ["open", "settled", "void"]},
"amount": {"bsonType": "double", "minimum": 0},
},
}
def configure(db, action):
if action is None:
db.command("collMod", "bench", validator={}, validationLevel="off")
else:
db.command("collMod", "bench", validator={"$jsonSchema": SCHEMA},
validationLevel="strict", validationAction=action)
3. Run the timed harness. Measure per-write latency, then report throughput and P95 — a mean hides the tail that validation actually moves:
import time
from statistics import quantiles
from pymongo import MongoClient
from pymongo.errors import WriteError
def bench(action, n=20000):
db = MongoClient("mongodb://primary:27017").bench_db
db.bench.drop()
db.create_collection("bench")
configure(db, action)
lat = []
start = time.perf_counter()
for i in range(n):
doc = make_doc(i)
t0 = time.perf_counter()
try:
db.bench.insert_one(doc)
except WriteError:
pass # under error, a rejected write is a real, timed outcome
lat.append((time.perf_counter() - t0) * 1000)
elapsed = time.perf_counter() - start
p95 = quantiles(lat, n=100)[94]
print(f"{str(action):>5}: {n/elapsed:8.0f} writes/s P95={p95:6.3f} ms")
for action in (None, "warn", "error"):
bench(action)
4. Read the shape, not the absolute numbers. Illustrative output from one run on a single-node replica set — your figures will differ, so gather your own:
None: 18400 writes/s P95= 0.071 ms # no schema-eval cost
warn: 14200 writes/s P95= 0.104 ms # full eval + persists + logs failures
error: 15100 writes/s P95= 0.096 ms # full eval, rejected writes skip persistence
The takeaway repeats across environments: the big step is off → validator-attached (the schema-eval tax), while warn versus error is a smaller, mix-dependent difference where error often edges ahead precisely because rejected writes do no storage work. The deeper rationale for choosing between the two in a live system, beyond raw throughput, is laid out in setting up validationAction warn vs error in production.
Failure Modes & Rollback
Benchmarks and rollouts fail in predictable ways; each has a bounded recovery:
- Measuring
warnon a dirty collection. If the collection already holds non-conforming documents,warnkeeps adding more during the run and your correctness numbers become meaningless. Reset with a fresh collection between runs; recovery is adroppluscreate_collection. - Regex cost swamps the signal. An unanchored
patternmakes bothwarnanderrorlook catastrophically slow and hides the action comparison. Anchor every regex with^/$, re-run, and the eval column drops sharply — time-to-recover is one schema edit. warnleft on in production after the study. The most expensive failure is not latency but silent data drift: a collection parked inwarnaccumulates malformed documents indefinitely. Promote toerrorthe moment drift is remediated.
Reverting any configuration is a single metadata-only collMod, effective on the primary immediately and propagated through the oplog within replication lag:
// Stop enforcing but keep the schema attached for telemetry.
db.runCommand({ collMod: "orders", validationLevel: "moderate", validationAction: "warn" })
// Remove the validator entirely (restores the baseline write path).
db.runCommand({ collMod: "orders", validator: {}, validationLevel: "off" })
Frequently Asked Questions
Is warn faster than error because it does not reject writes?
No — that is the common misconception this page corrects. Both actions run the full $jsonSchema evaluation on every write, so they share the identical CPU cost. On the failure branch, error rejects and does no persistence, while warn persists the document and writes an extra mongod log line. In workloads with a meaningful violation rate, error often measures slightly faster because rejected writes skip journal, index, and log I/O entirely.
What dominates the schema-evaluation cost that both modes pay?
Schema shape, not action. Deeply nested allOf chains multiply the number of subschemas evaluated per document, and an unanchored pattern regex forces a match attempt at every character offset, turning a field check into a super-linear scan on long strings. Anchor every regex with the start and end tokens and flatten redundant combinators before you conclude that validation itself is expensive.
Can I trust published benchmark numbers for my deployment?
Treat all such numbers, including the illustrative figures here, as directional only. Absolute throughput depends on document size, schema complexity, hardware, write concurrency, and replication configuration. Run the harness against your own schema and representative documents on your own hardware, and report throughput plus P95 latency rather than a single mean, which hides the tail that validation actually shifts.
Related
- Strict vs Moderate Validation Levels — the parent guide to how
validationLevelandvalidationActioncombine into an enforcement contract. - MongoDB JSON Schema Validation Architecture — the section reference defining the write-path evaluation pipeline these benchmarks measure.
- Bypassing Validation with bypassDocumentValidation Safely — the sibling covering the one path that skips schema-eval cost entirely, and why that is dangerous.
- Setting up validationAction warn vs error in production — the operational decision framework behind the mode you benchmark here.
- How to enforce strict validation on existing collections — the zero-downtime rollout that promotes a measured
warnwindow toerror.