Prompt 18 · Lesson 03 · Event payload semantics

Full Document/Pre-Image Options, Update Descriptions, Deletes, and Schema Evolution

An update delta, a current lookup, and retained pre/post images answer different questions. Choose the payload according to the consumer invariant.

Intermediate–Advanced120–190 minutesCDC/resumability engineering labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Distinguish updateDescription deltas, updateLookup current-document lookup, stored post-images, and pre-images.

02

Enable collection pre/post images and choose whenAvailable versus required according to failure tolerance.

03

Handle delete events where the current document no longer exists.

04

Build schema-tolerant consumers that accept update and replace events and version their downstream contract.

05

Account for pre-image storage/retention and the 16 MiB change-event BSON limit.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo 4.17.0. The mandatory lab uses a disposable single-member replica set on loopback port 27157; change streams require a replica set or sharded cluster, so a standalone is intentionally not used. A single-member replica set is enough to learn event/resume mechanics but does not demonstrate high availability, multi-node majority durability, or failover. Authentication and TLS are disabled only for this isolated lab. Feature Compatibility Version (FCV) is inspected. FCV is observed and never changed. Default read/write concern and primary read preference apply unless a command says otherwise. The collection enables changeStreamPreAndPostImages. Pre-images are stored in config.system.preimages and add storage/processing cost; the lesson inspects but never directly mutates that internal collection. Atlas, Search, Vector Search, KMS, and Enterprise Advanced are not mandatory. Product commands were not executed in this generation environment because Docker, mongod, mongosh, and PyMongo are unavailable here; runtime timings and token values must be measured on the learner machine rather than copied as invented output.

start disposable replica set (l3)
docker rm -f atlasmart-ch18-l3 2>/dev/null || truedocker volume rm atlasmart-ch18-l3-data 2>/dev/null || truedocker run -d --name atlasmart-ch18-l3 \  -p 127.0.0.1:27157:27017 \  -v atlasmart-ch18-l3-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs18-l3 --oplogSize 128 --bind_ip_alluntil mongosh "mongodb://127.0.0.1:27157/admin?directConnection=true" --quiet --eval 'quit(db.runCommand({ping:1}).ok===1?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27157/admin?directConnection=true" --quiet --eval 'rs.initiate({_id:"atlasmart-rs18-l3",members:[{_id:0,host:"atlasmart-ch18-l3:27017"}]})'until mongosh "mongodb://127.0.0.1:27157/admin?directConnection=true" --quiet --eval 'quit(db.hello().isWritablePrimary?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27157/admin?replicaSet=atlasmart-rs18-l3" --quiet --eval 'printjson(db.version());printjson(db.runCommand({getParameter:1,featureCompatibilityVersion:1}).featureCompatibilityVersion);printjson(rs.status().members.map(m=>({name:m.name,stateStr:m.stateStr})));' 

1. Four payload ideas that are easy to conflate

For an update, updateDescription describes the changed/removed fields. fullDocument:"updateLookup" performs a later lookup of the current majority-committed document; intervening writes can make that lookup newer than the event's delta. MongoDB 6.0+ pre/post images instead preserve document images associated with the change when the feature was enabled and the image remains retained. A delete has no current post-image because the document no longer exists.

Payload Meaning Important edge
updateDescription Delta for the update event. Consumer must understand paths/array truncation and tolerate schema evolution.
fullDocument:"updateLookup" Current majority-committed document when lookup executes. May include later changes and may be missing if document/namespace changed.
fullDocument:"whenAvailable" Stored post-image when available. Requires pre/post-image feature for update post-image semantics.
fullDocumentBeforeChange Stored pre-image before update/replace/delete. Can be unavailable because feature was off or retention removed it.

2. Enable images and capture update + delete evidence

create collection with pre/post images
const app=db.getSiblingDB("atlasmart");app.products_ch18_l3.drop();app.createCollection("products_ch18_l3", {changeStreamPreAndPostImages:{enabled:true}});app.products_ch18_l3.insertOne({_id:"p-3",schemaVersion:1,name:"Router",price:99,tags:["network"]});printjson(app.getCollectionInfos({name:"products_ch18_l3"})[0].options);printjson(db.getSiblingDB("config").system.preimages.stats());
watch pre/post images and inspect schema-sensitive events
from pprint import pprintfrom pymongo import MongoClientclient = MongoClient("mongodb://127.0.0.1:27157/?replicaSet=atlasmart-rs18-l3")c = client.atlasmart.products_ch18_l3with c.watch(    full_document="whenAvailable",    full_document_before_change="whenAvailable",    max_await_time_ms=1000,) as stream:    c.update_one({"_id":"p-3"},{"$set":{"schemaVersion":2,"price":89,"attributes":{"wifi":"6E"}}})    update_event = stream.next()    pprint({        "op": update_event["operationType"],        "delta": update_event.get("updateDescription"),        "before": update_event.get("fullDocumentBeforeChange"),        "after": update_event.get("fullDocument"),    })    c.delete_one({"_id":"p-3"})    delete_event = stream.next()    pprint({        "op": delete_event["operationType"],        "documentKey": delete_event["documentKey"],        "before": delete_event.get("fullDocumentBeforeChange"),        "after": delete_event.get("fullDocument"),    })client.close()

Expected invariant: the update has a delta plus available pre/post image; the delete retains documentKey and can have a pre-image, but there is no post-delete document. Exact resume tokens and timestamps are runtime values.

3. required versus whenAvailable is a correctness decision

whenAvailable lets the stream continue with a null/missing image if the image is unavailable. required asks the server to fail when the requested image cannot be returned. Use required only when missing an image is itself a correctness failure and the retention/storage design supports that contract. Pre-images are removed asynchronously and also disappear when their corresponding change event falls out of the oplog, regardless of a longer configured image retention period.

16 MiB event boundary

A change event is a BSON document and must remain under MongoDB's 16 MiB BSON document limit. Asking simultaneously for a large pre-image, post-image, and a large updateDescription can exceed that limit. Keep watched documents bounded and request only the payload needed by the consumer.

4. Deliberately wrong consumer: “all changes are update events with fullDocument”

MongoDB may represent a replacement as replace rather than update, and delete events have no current document. A consumer that assumes operationType=="update" and blindly dereferences fullDocument will silently miss replacements or crash on deletes. Normalize operations into an application-owned event envelope and keep schema versioning explicit.

schema-tolerant event normalization
def normalize(change):    op = change["operationType"]    if op in {"insert", "replace", "update"}:        doc = change.get("fullDocument")        return {            "kind": "product-upsert",            "sourceOp": op,            "id": change["documentKey"]["_id"],            "schemaVersion": (doc or {}).get("schemaVersion"),            "document": doc,            "delta": change.get("updateDescription"),        }    if op == "delete":        return {            "kind": "product-delete",            "sourceOp": op,            "id": change["documentKey"]["_id"],            "before": change.get("fullDocumentBeforeChange"),        }    return {"kind":"control-or-unknown", "sourceOp":op}

5. Production judgment

Use deltas when downstream state can apply patches safely; use post-images when consumers need a full snapshot; use pre-images for audit/compensation only when the additional storage, retention, privacy, and event-size costs are justified. Treat image retention as part of the consumer recovery objective, and monitor config.system.preimages size. Do not persist sensitive pre-images indefinitely just because CDC makes it convenient.

Bridge. Lesson 4 turns the resume token into a durable recovery protocol and shows the special invalidation/history-loss boundaries that ordinary reconnect logic cannot hide.

Cleanup/reset

Everything in this lesson is disposable. Remove only the chapter-specific container and volume:

cleanup
docker rm -f atlasmart-ch18-l3 2>/dev/null || truedocker volume rm atlasmart-ch18-l3-data 2>/dev/null || true

Check your understanding

  1. Does updateLookup return the exact document as of the update event?
  2. Can a delete event have fullDocument after the delete?
  3. What is the difference between whenAvailable and required?
  4. Why must consumers handle replace as well as update?
  5. What operational cost do pre-images add?
Review the answers

1. Not necessarily; it returns the current majority-committed version when the lookup executes, so later writes can be reflected.

2. No current post-image exists after deletion; use documentKey and optionally a retained pre-image.

3. whenAvailable tolerates an unavailable image; required fails when the requested image cannot be returned.

4. MongoDB can represent a replacement as a replace event; filtering only update can miss meaningful changes.

5. Additional storage/processing, retention management, privacy surface, and potentially larger change events.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.