Production Best Practices
The patterns in this tutorial work in development on a local server with a few thousand documents. Production adds the dimensions development hides: pool sizing, write safety, index maintenance, data growth, and failure behavior. This chapter is the checklist that closes the gap - the settings, habits, and monitoring that keep a MongoEngine application correct and fast at real scale.
Connections and Pooling
- Size the pool to concurrency. The driver's pool (default 100 per server) should roughly match your concurrent request capacity; thousands of app threads against a 100-pool is queueing, not parallelism. Set
maxPoolSizein the connection URI deliberately. - One long-lived connection per database per process. Call
connectonce at startup; the pool persists for the process lifetime. Reconnecting per request is a bug. - Set timeouts.
serverSelectionTimeoutMS(default 30 s) bounds how long a query waits for a server; a hung connection should surface as an error your code handles, not a frozen worker.
Write Safety
- Acknowledged writes by default. The driver's default write concern waits for server acknowledgment - keep it. Unacknowledged writes are a latency optimization that trades correctness, and almost no application can afford the trade.
- Use atomic updates for shared counters and flags (the
inc__/set__forms) instead of read-modify-write, or two workers will lose updates. - Idempotency for retries. A timeout does not mean the write failed - it means the answer was not heard. Design writes (or use a natural unique key) so a retry cannot double-apply.
- Bulk operations with
ordered=Falsefor cohort jobs where one failure should not abort the batch.
Indexes and Data Growth
- Run
ensure_indexes()in the deploy step, not just at first import - new deploys add new indexes, and idempotent creation makes it safe to call on every release. - Watch unbounded arrays. The 16 MB document limit is the hard wall; review every
ListFieldthat can grow (comments, events, tags) and cap, truncate, or move it to its own collection. - TTL indexes for expiring data (sessions, jobs, audit windows):
{"expireAfterSeconds": 86400}on the timestamp field, and MongoDB reaps the rows for you. - Archive, do not just delete. Cold data moves to an archive collection (or out of the database) so hot collections - and their indexes - stay small and fast.
Query Discipline
- No
COLLSCANon a hot path. Runexplain()for every query that serves a request; the examined-to-returned ratio is the number to watch, and it is what index design optimizes. - Project with
only()- list endpoints fetch the fields the template renders, not the whole document. - Cap every queryset that can grow (
[:100]); an unboundedfor doc in qsin a request handler is a memory and latency incident waiting for a data-growth quarter. - Kill N+1 dereferences with the bulk
in_load pattern (the querying-relationships chapter) - one query for the referenced set, not one per row.
Monitoring and Operations
- Slow query log. Enable it with a low threshold in staging; in production, a threshold above your p99 request budget. It is the ground truth for "which query got slow and why".
- Track connection pool state (checked-out vs available) - a pool pinned at its ceiling is the signature of a leak or a pool-size mismatch.
- Monitor collection and index sizes over time; a collection that doubles every month is a planning fact, not a surprise.
- Backups and a tested restore. A backup that has never been restored is a hope, not a backup - run the restore in staging on a schedule.
Code-Level Habits
strict = Truein production models so the class is the schema and stray keys cannot silently accumulate.- Version your schema changes like code: a migration or deploy step that documents what changed, and
ensure_indexes()as part of it. - Pin and record driver versions (mongoengine and pymongo) in the requirements file.
- Keep a raw escape hatch (
as_pymongo()) but route it through a thin data-access layer so the raw calls are findable in review.
Summary
Production MongoEngine is the tutorial's patterns plus deliberate operations: sized and timed connection pools, acknowledged and idempotent writes, atomic updates for shared state, indexes ensured on every deploy, bounded arrays and TTL/archiving for growth, explain()-verified queries with only() projections, and a slow-query log plus pool monitoring that make degradation visible before users do. The model code barely changes - the difference is that every assumption development made for you is now something you configured.