<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saqib Ameen Subhan</title>
    <description>The latest articles on DEV Community by Saqib Ameen Subhan (@saqibameen86).</description>
    <link>https://dev.to/saqibameen86</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049802%2F2d78cd6c-e644-4178-86b6-6a4638bfe5a4.jpg</url>
      <title>DEV Community: Saqib Ameen Subhan</title>
      <link>https://dev.to/saqibameen86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saqibameen86"/>
    <language>en</language>
    <item>
      <title>Large partitions don't fail loudly. They fail everywhere at once.</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:29:00 +0000</pubDate>
      <link>https://dev.to/saqibameen86/large-partitions-dont-fail-loudly-they-fail-everywhere-at-once-2j41</link>
      <guid>https://dev.to/saqibameen86/large-partitions-dont-fail-loudly-they-fail-everywhere-at-once-2j41</guid>
      <description>&lt;p&gt;Most Cassandra problems announce themselves. A timeout, an exception, a graph going vertical.&lt;/p&gt;

&lt;p&gt;Large partitions do not. They degrade reads, compaction, repair and GC &lt;em&gt;simultaneously and gradually&lt;/em&gt;, which is why the incident channel fills with four unrelated looking symptoms and no suspect.&lt;/p&gt;

&lt;p&gt;If you have been following this series, you have already met large partitions twice without the name. They are the humongous allocations in the heap dump post, and they are the overstreaming amplifier in the repair post. This post is about the disease itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What large means and why it hurts
&lt;/h2&gt;

&lt;p&gt;A partition is the unit Cassandra reads, compacts, streams and materializes. The rule of thumb that has served me well is to &lt;strong&gt;aim under 100MB per partition&lt;/strong&gt; and treat anything past that as debt. Old timers remember the hard 2GB era limits. Modern Cassandra handles big partitions better, which mostly means they hurt you more gradually.&lt;/p&gt;

&lt;p&gt;Where the pain lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads.&lt;/strong&gt; A partition read materializes structures proportional to what is scanned. In G1 terms, a multi hundred MB partition read is a parade of humongous allocations. GC pause spikes on whichever node holds it, the cluster marks it slow, and speculative retries fan the load elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compaction.&lt;/strong&gt; Partitions are compacted as units. Giant partitions make giant, slow compaction tasks that starve neighbours, and pending tasks climb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair.&lt;/strong&gt; One mismatched cell in a giant partition can stream the whole thing. The overstreaming problem, concentrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotspotting.&lt;/strong&gt; Big partitions are usually &lt;em&gt;hot&lt;/em&gt; partitions, the same key taking disproportionate traffic. Size and heat compound.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How partitions get big, always the same story
&lt;/h2&gt;

&lt;p&gt;The partition key models an unbounded thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- looks innocent in the design review&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_by_device&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="nb"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partition key of &lt;code&gt;device_id&lt;/code&gt; means every event that device ever produces, forever, in one partition. A chatty device, a popular customer, a busy trading day. Growth has no ceiling because the model gave it none.&lt;/p&gt;

&lt;p&gt;I have seen the one big customer partition at multiple companies. It is practically a genre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nodetool tablehistograms ks events_by_device
&lt;span class="c"&gt;# Percentile      Partition Size&lt;/span&gt;
&lt;span class="c"&gt;# 50%             61,214 bytes&lt;/span&gt;
&lt;span class="c"&gt;# 99%             386,857,368 bytes     &amp;lt;- there it is&lt;/span&gt;
&lt;span class="c"&gt;# Max             2,395,318,855 bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The p50 to p99 gap is the signature. A healthy median hiding a monster tail.&lt;/p&gt;

&lt;p&gt;To name the offenders on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# per SSTable, list partitions over a threshold&lt;/span&gt;
sstablepartitions &lt;span class="nt"&gt;-t&lt;/span&gt; 100 /var/lib/cassandra/data/ks/events_by_device-&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Cassandra logs them at write time. Grep &lt;code&gt;system.log&lt;/code&gt; for &lt;code&gt;Writing large partition&lt;/code&gt;. Those log lines are the cheapest early warning system you will ever ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: give the partition a ceiling
&lt;/h2&gt;

&lt;p&gt;You cannot cap a device's lifetime events. You &lt;em&gt;can&lt;/em&gt; cap a partition, by putting time into the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_by_device_v2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;-- the bucket&lt;/span&gt;
  &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="nb"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a partition is one device, one day. Bounded by physics instead of hope. Two design decisions follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bucket size is a calculation, not a vibe.&lt;/strong&gt; Estimate rows per day times row size, then pick the bucket, hour, day or week, that lands typical partitions in single digit MB and your worst realistic case under about 100MB. Do the arithmetic for your &lt;em&gt;loudest&lt;/em&gt; tenant, not your average one. Averages are how the enormous customer surprises you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi bucket reads are now yours to orchestrate.&lt;/strong&gt; Last 6 hours spanning midnight means two partitions. Drivers make querying a known list of buckets cheap. Just design the access pattern alongside the schema, not after it. This pairs well with TWCS from the compaction post, because bucketed, TTL'd time series is the workload TWCS was born for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration reality.&lt;/strong&gt; Existing giants must be rewritten into v2. Dual write new data, backfill old in throttled batches, the same discipline as the 2TB MongoDB delete earlier in this series, chunk, sleep, watch the cluster. Then cut reads over and drop v1. Weeks, calmly, not a weekend, heroically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The review question that prevents all of it
&lt;/h2&gt;

&lt;p&gt;Every Cassandra schema review I run ends with one question. &lt;em&gt;What bounds this partition?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is a business quantity, customers will not have many orders, then it is unbounded, because business quantities grow. That is their job. The only acceptable answers have units of time, or a hard modulo in the key.&lt;/p&gt;

&lt;p&gt;Large partitions are never an ops problem. They are a design review that ended one question too early.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>datamodeling</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>Repair at 1,000 nodes: why we stopped running nodetool repair by hand</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:28:30 +0000</pubDate>
      <link>https://dev.to/saqibameen86/repair-at-1000-nodes-why-we-stopped-running-nodetool-repair-by-hand-289i</link>
      <guid>https://dev.to/saqibameen86/repair-at-1000-nodes-why-we-stopped-running-nodetool-repair-by-hand-289i</guid>
      <description>&lt;p&gt;At small scale, repair is a cron job. At 1,000 nodes it is a scheduling problem, and if you treat a scheduling problem like a cron job, the cluster will teach you the difference during business hours.&lt;/p&gt;

&lt;p&gt;I spent years operating Cassandra estates past the thousand node mark. Here is what repair actually does, why it is not optional, and how the operational model has to change with scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why repair is a deadline, not a chore
&lt;/h2&gt;

&lt;p&gt;Cassandra replicas drift. Hinted handoff covers short outages, but hints have a window, three hours by default. A replica down longer than that has permanently missed writes, and only &lt;strong&gt;anti entropy repair&lt;/strong&gt; reconciles it.&lt;/p&gt;

&lt;p&gt;The part that turns repair from hygiene into a deadline is tombstones. Deleted data is protected from resurrection by grave markers that compaction may purge after &lt;code&gt;gc_grace_seconds&lt;/code&gt;, ten days by default. The safety of that purge rests on one assumption. &lt;strong&gt;Every replica heard about the delete before the marker vanished.&lt;/strong&gt; Repair is how they hear.&lt;/p&gt;

&lt;p&gt;So the rule is absolute. &lt;strong&gt;Every node completes repair at least once per &lt;code&gt;gc_grace_seconds&lt;/code&gt;.&lt;/strong&gt; Miss it, and a replica that slept through a delete will happily hand the missing row back to the cluster. Zombie data, silent, and by the time anyone notices, days of writes have been made against resurrected garbage.&lt;/p&gt;

&lt;p&gt;Repair is not cleanup. It is the second half of every delete you have ever issued.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a repair costs
&lt;/h2&gt;

&lt;p&gt;Mechanically, nodes build &lt;strong&gt;Merkle trees&lt;/strong&gt; over their data ranges, exchange them, and stream any ranges whose hashes disagree. Three costs follow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building trees reads data.&lt;/strong&gt; Sequential I/O competing with your workload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming mismatches&lt;/strong&gt; saturates network and creates new SSTables, which means the third cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compaction debt.&lt;/strong&gt; A big repair is followed by a compaction hangover, and &lt;code&gt;nodetool compactionstats&lt;/code&gt; pending tasks tell the story.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Merkle tree resolution is finite, so one tree over a huge range means one tiny mismatch streams a disproportionately large chunk. This is &lt;strong&gt;overstreaming&lt;/strong&gt;, and it is why repairing a giant range in one shot is both slow and wasteful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full, incremental, subrange, and what breaks at scale
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Full repair&lt;/strong&gt;, whole range, everything rehashed every time. Correct, brutal, does not scale. At 1,000 nodes, naive &lt;code&gt;nodetool repair&lt;/code&gt; across the fleet means the cluster is effectively always repairing, and repair sessions colliding on shared ranges fail in tedious ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental repair&lt;/strong&gt; marks SSTables repaired so they are skipped next time. On paper, the fix. In practice it drags &lt;strong&gt;anticompaction&lt;/strong&gt; behind it, rewriting SSTables to segregate repaired from unrepaired data, and its operational history, particularly before Cassandra 4.0's fixes, was rocky enough that many large operators simply did not trust it. We did not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subrange repair&lt;/strong&gt; breaks the token range into small segments repaired one at a time. Small Merkle trees, precise comparisons, minimal overstreaming, and each segment is a small retryable unit of work. This is the primitive that actually scales.&lt;/p&gt;

&lt;p&gt;But subrange repair across 1,000 nodes is thousands upon thousands of segments needing ordering, throttling, retries and collision avoidance. Congratulations, it is a scheduling problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reaper: repair as a system, not a command
&lt;/h2&gt;

&lt;p&gt;We ran &lt;strong&gt;Cassandra Reaper&lt;/strong&gt;, open source, originally Spotify's and now community maintained, as the scheduler.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Splits every table's range into &lt;strong&gt;segments&lt;/strong&gt; and runs them steadily with concurrency caps&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;intensity&lt;/strong&gt; knob throttles repair pressure so p99s do not feel it. We tuned it to stay invisible.&lt;/li&gt;
&lt;li&gt;Failed segments retry individually, so a node blip costs one segment, not the whole run&lt;/li&gt;
&lt;li&gt;A web UI that answers the only question leadership asks, which is whether we are inside the gc_grace window, yes or no&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operating policy that came out of years of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- every table repaired well inside gc_grace, target a complete cycle in ~7 days against a 10 day grace
- repair pressure tuned to be invisible in p99 read latency
- alert not on "repair failed" but on "time since last completed repair per table"
- pause repairs during topology changes, resume, never skip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last alert framing matters. Failures are noise. &lt;em&gt;Staleness&lt;/em&gt; is the actual risk. Measure the deadline, not the activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small scale corollary
&lt;/h2&gt;

&lt;p&gt;Under about 50 nodes you do not need Reaper's ceremony, but you do need the same two invariants. Every node repaired inside &lt;code&gt;gc_grace_seconds&lt;/code&gt;, and repairs that do not collide. A boring, monitored schedule beats heroics.&lt;/p&gt;

&lt;p&gt;Repair is the tax on eventual consistency. You can pay it on a schedule you chose, or all at once with interest, with zombie data as the collection notice.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>operations</category>
      <category>distributed</category>
      <category>database</category>
    </item>
    <item>
      <title>The heap dump usually exonerates Cassandra</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:26:34 +0000</pubDate>
      <link>https://dev.to/saqibameen86/the-heap-dump-usually-exonerates-cassandra-406o</link>
      <guid>https://dev.to/saqibameen86/the-heap-dump-usually-exonerates-cassandra-406o</guid>
      <description>&lt;p&gt;When a Cassandra node OOMs, or GC pauses start stretching into seconds, the reflex is to blame Cassandra, or its favourite scapegoat, the JVM.&lt;/p&gt;

&lt;p&gt;I have read a lot of Cassandra heap dumps, and here is the uncomfortable pattern. The heap usually contains exactly what the data model asked for. The dump does not convict the database. It convicts a partition.&lt;/p&gt;

&lt;p&gt;This post is the workflow I actually use, in order, because the order saves hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: GC logs before heap dumps
&lt;/h2&gt;

&lt;p&gt;A heap dump is a biopsy. GC logs are the patient history, and they are already on disk. Two things I look for first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# gc.log (G1)
Pause Young (Normal) ... 180ms
Pause Young (Normal) ... 210ms
Pause Full (Allocation Failure) ... 8.4s      &amp;lt;- the incident
...
Humongous Allocation ... 18MB
Humongous Allocation ... 22MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Full GC pauses.&lt;/strong&gt; G1 doing an emergency stop the world collection because normal cycles could not keep up. On a Cassandra node, an 8 second pause does not just slow queries. The node misses gossip, gets marked down by peers, and hints start piling up elsewhere. One node's GC problem becomes the cluster's problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Humongous allocations.&lt;/strong&gt; G1 divides the heap into regions, typically 4 to 32MB. Any single allocation larger than &lt;strong&gt;half a region&lt;/strong&gt; is humongous, allocated awkwardly across contiguous regions and collected inefficiently.&lt;/p&gt;

&lt;p&gt;And what does Cassandra allocate in one giant contiguous chunk? A large partition being materialized for a read. Humongous allocation warnings in a Cassandra gc.log are large partitions announcing themselves. You can often skip the heap dump entirely at this point and go straight to &lt;code&gt;nodetool tablehistograms&lt;/code&gt; to find the table with a monster p99 partition size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: take the dump without causing an incident
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;jmap&lt;/code&gt; on a live heap &lt;strong&gt;stops the world for the duration of the dump&lt;/strong&gt;. On a 20GB heap, that is long enough for the cluster to declare the node dead. Never dump a node that is still serving.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# isolate first, node stays alive but stops serving&lt;/span&gt;
nodetool disablebinary &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; nodetool disablegossip

jmap &lt;span class="nt"&gt;-dump&lt;/span&gt;:live,format&lt;span class="o"&gt;=&lt;/span&gt;b,file&lt;span class="o"&gt;=&lt;/span&gt;/data/dumps/cass_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;.hprof &amp;lt;pid&amp;gt;

&lt;span class="c"&gt;# afterwards: rejoin or just restart the node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better, have the JVM do it for you at the moment of truth, before any human is awake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;-XX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;+HeapDumpOnOutOfMemoryError&lt;/span&gt;
&lt;span class="py"&gt;-XX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;HeapDumpPath=/data/dumps/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two flags belong in every production Cassandra JVM config. An OOM without a dump is an incident you get to have twice.&lt;/p&gt;

&lt;p&gt;Related discipline, keep the heap at or under 8GB and let G1 target around 200ms pauses. Bigger heaps mostly buy you longer pauses and bigger dumps, and Cassandra's real caching happens off heap and in the page cache anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Eclipse MAT, dominator tree, ten minutes
&lt;/h2&gt;

&lt;p&gt;Open the &lt;code&gt;.hprof&lt;/code&gt; in Eclipse MAT and go straight to the &lt;strong&gt;dominator tree&lt;/strong&gt;, objects ranked by retained memory, which answers what is actually holding the heap hostage. Ignore the histogram of a billion &lt;code&gt;byte[]&lt;/code&gt;. The dominator tree tells you &lt;em&gt;whose&lt;/em&gt; bytes.&lt;/p&gt;

&lt;p&gt;What the tree shows, mapped to what it means, every one of these from real incidents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dominating the heap&lt;/th&gt;
&lt;th&gt;Actual problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A few enormous &lt;code&gt;byte[]&lt;/code&gt; or cell containers under a read path&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Large partition&lt;/strong&gt; being materialized. Get the key from the referencing objects, confirm with &lt;code&gt;nodetool tablehistograms&lt;/code&gt; or &lt;code&gt;sstablepartitions&lt;/code&gt;. Fix is to bucket the partition, which is a later post in this series.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memtable objects, many tables&lt;/td&gt;
&lt;td&gt;Over wide schema, or memtable flush thresholds set too generous for the heap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tombstone or range tombstone structures under a query&lt;/td&gt;
&lt;td&gt;A tombstone farm read that survived just long enough to dump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netty buffers or inflight requests&lt;/td&gt;
&lt;td&gt;Clients hammering with no paging. Check fetch size and unpaged full partition reads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The punchline repeats across years and companies. &lt;strong&gt;The heap contains the workload.&lt;/strong&gt; I have almost never opened a dump and found a Cassandra bug. I have regularly opened one and found a 4GB partition someone swore was just a busy customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: close the loop in the model
&lt;/h2&gt;

&lt;p&gt;The dump names the object. The fix lives in the schema.&lt;/p&gt;

&lt;p&gt;Large partition, time bucket the partition key. Unpaged reads, driver fetch size. Tombstone reads, the modeling fixes from two posts ago. The JVM flags and MAT are diagnosis. Cassandra data modeling is treatment.&lt;/p&gt;

&lt;p&gt;Blaming the JVM is comfortable because nobody owns the JVM. The heap dump takes that comfort away. It shows you, in retained bytes, precisely which design decision you are looking at.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>jvm</category>
      <category>performance</category>
      <category>debugging</category>
    </item>
    <item>
      <title>A compaction strategy is a bet about your workload</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:25:24 +0000</pubDate>
      <link>https://dev.to/saqibameen86/a-compaction-strategy-is-a-bet-about-your-workload-200k</link>
      <guid>https://dev.to/saqibameen86/a-compaction-strategy-is-a-bet-about-your-workload-200k</guid>
      <description>&lt;p&gt;Choosing a Cassandra compaction strategy is placing a bet. Each strategy optimizes one thing by deliberately sacrificing another, and the honest way to choose is to decide &lt;strong&gt;which failure mode you would rather operate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I have run all three majors across 1,000+ node estates. Here is the bet each one makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why compaction exists at all
&lt;/h2&gt;

&lt;p&gt;Cassandra never updates in place. Writes land in a memtable, flush to immutable SSTables, and a row's fragments accumulate across many files as it is updated over time. Reads must merge every relevant fragment.&lt;/p&gt;

&lt;p&gt;Compaction is the background process that merges SSTables, consolidating fragments and purging expired tombstones after &lt;code&gt;gc_grace_seconds&lt;/code&gt;, so reads touch fewer files.&lt;/p&gt;

&lt;p&gt;The metric that keeps compaction honest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nodetool tablehistograms ks table
&lt;span class="c"&gt;# Percentile  SSTables     ...&lt;/span&gt;
&lt;span class="c"&gt;# 50%             1.00&lt;/span&gt;
&lt;span class="c"&gt;# 99%             4.00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSTables per read at p99 is the number compaction exists to keep small.&lt;/p&gt;

&lt;h2&gt;
  
  
  STCS, SizeTiered, the write optimized default
&lt;/h2&gt;

&lt;p&gt;Merges SSTables of similar size into bigger ones, repeatedly. Minimal write amplification, maximal ingest throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bet you are making:&lt;/strong&gt; reads and disk headroom are negotiable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A hot row's fragments can sit in many SSTables of different generations, so SSTables per read climbs.&lt;/li&gt;
&lt;li&gt;Space amplification is the operational trap. Compacting large tiers requires holding input &lt;em&gt;and&lt;/em&gt; output simultaneously. Plan for &lt;strong&gt;50% free disk&lt;/strong&gt;. I have watched teams treat 70% disk usage as plenty left, then be unable to run the very compaction that would reclaim space. That is a corner with no good exits.&lt;/li&gt;
&lt;li&gt;Tombstones buried in giant, rarely recompacted SSTables can linger far past &lt;code&gt;gc_grace_seconds&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Right when:&lt;/strong&gt; write heavy, append mostly, reads tolerate variance. It is the default because it is the least dangerous &lt;em&gt;average&lt;/em&gt; bet, not because it is right for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  LCS, Leveled, paying writes to buy reads
&lt;/h2&gt;

&lt;p&gt;Organizes SSTables into levels, L1, L2 and so on, each 10 times larger, guaranteeing non overlapping key ranges within a level. A read touches at most one SSTable per level, and in practice the vast majority of reads are satisfied by a single SSTable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bet:&lt;/strong&gt; you will pay roughly &lt;strong&gt;10x write amplification&lt;/strong&gt;, because every row is rewritten as it migrates down levels, to make read latency tight and predictable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On read heavy tables with strict p99s, it is the correct bet.&lt;/li&gt;
&lt;li&gt;On write heavy tables, compaction falls behind, pending tasks pile up in &lt;code&gt;nodetool compactionstats&lt;/code&gt;, and ironically reads degrade anyway because L0 accumulates overlapping files.&lt;/li&gt;
&lt;li&gt;Easier on disk headroom than STCS, works in around 10% free, but harder on I/O and CPU, continuously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Right when:&lt;/strong&gt; read dominated, update in place workloads. Wrong when your disks are already busy keeping up with ingest.&lt;/p&gt;

&lt;h2&gt;
  
  
  TWCS, TimeWindow, the time series specialist
&lt;/h2&gt;

&lt;p&gt;Groups SSTables by time window, say one per day. Within the current window, STCS as usual. Once a window closes, its SSTables compact once into one file and are &lt;strong&gt;never touched again&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When TTL expires a whole window, Cassandra drops the entire file, so tombstone processing effectively vanishes for aged out data. This is the single biggest tombstone fix available for time series, and the punchline of the previous post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bet:&lt;/strong&gt; your data is truly time ordered and immutable once written.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write out of order data or update old windows and you create cross window overlaps that can never compact away. The strategy's guarantees quietly die while everything still appears to work.&lt;/li&gt;
&lt;li&gt;Rule of thumb, aim for a window size that yields &lt;strong&gt;20 to 50 windows&lt;/strong&gt; over the table's TTL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Right when:&lt;/strong&gt; metrics, events, telemetry, anything with TTL and append only semantics. IoT and monitoring tables at scale should almost always be TWCS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bet, stated plainly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;You optimize&lt;/th&gt;
&lt;th&gt;You pay with&lt;/th&gt;
&lt;th&gt;Operational trap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STCS&lt;/td&gt;
&lt;td&gt;write throughput&lt;/td&gt;
&lt;td&gt;read variance&lt;/td&gt;
&lt;td&gt;needs around 50% free disk to compact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCS&lt;/td&gt;
&lt;td&gt;read p99&lt;/td&gt;
&lt;td&gt;10x write amplification&lt;/td&gt;
&lt;td&gt;falls behind under heavy ingest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TWCS&lt;/td&gt;
&lt;td&gt;TTL'd time series&lt;/td&gt;
&lt;td&gt;inflexibility&lt;/td&gt;
&lt;td&gt;out of order writes break it silently&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per table, not per cluster. A keyspace legitimately mixes all three. And changing the bet is an online operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;compaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'TimeWindowCompactionStrategy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'compaction_window_unit'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'DAYS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'compaction_window_size'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cassandra will re sort existing SSTables over time. On a big table, schedule the transition like the migration it is.&lt;/p&gt;

&lt;p&gt;There is no best compaction strategy. There is only the failure mode you have chosen on purpose, versus the one that chose you.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>performance</category>
      <category>database</category>
      <category>operations</category>
    </item>
    <item>
      <title>mtools does not parse MongoDB 4.4+ JSON logs. So I built mdbkit.</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:28:11 +0000</pubDate>
      <link>https://dev.to/saqibameen86/mtools-does-not-parse-mongodb-44-json-logs-so-i-built-mdbkit-5a29</link>
      <guid>https://dev.to/saqibameen86/mtools-does-not-parse-mongodb-44-json-logs-so-i-built-mdbkit-5a29</guid>
      <description>&lt;p&gt;If you have operated MongoDB for any length of time, you know mtools. For years it was how we all read production logs. &lt;code&gt;mloginfo&lt;/code&gt;, &lt;code&gt;mplotqueries&lt;/code&gt;, and a slow query was suddenly visible.&lt;/p&gt;

&lt;p&gt;Then MongoDB 4.4 changed the log format to structured JSON, and the log tools stopped understanding what they were reading.&lt;/p&gt;

&lt;p&gt;That was six years ago. The support issue was opened in June 2020 and it is still open. We are on 8.0 now, and a lot of us have been getting by with &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt; pipelines held together by hope, and a certain amount of squinting.&lt;/p&gt;

&lt;p&gt;I got tired of it and built the replacement. It is called mdbkit, it is MIT licensed, and it is public as of this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in sixty seconds without touching a cluster
&lt;/h2&gt;

&lt;p&gt;This is the part I would want first if this were someone else's tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; mdbkit
&lt;span class="c"&gt;# macOS ships no pip: brew install pipx &amp;amp;&amp;amp; pipx install mdbkit&lt;/span&gt;

mdbkit demo &lt;span class="nt"&gt;-o&lt;/span&gt; demo.log
mdbkit queries demo.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mdbkit demo&lt;/code&gt; generates a realistic MongoDB log containing an actual incident. No cluster, no connection string, nothing of yours involved. You get to judge the tool on a problem you can inspect before you point it at anything that matters.&lt;/p&gt;

&lt;p&gt;Here is what &lt;code&gt;queries&lt;/code&gt; gives you on that demo log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;saqib@MacbookPro mongodb_logs % mdbkit queries demo.log
== mdbkit queries (slow query shapes) ==
&lt;/span&gt;&lt;span class="gp"&gt;lines: 1,004  parsed: 1,004  unparsed: 0  span: 2026-07-01T08:00:00+04:00 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;2026-07-01T09:29:44.615000+04:00
&lt;span class="go"&gt;
namespace      op         count  cumMs  mean   max    docsEx      scan     plan            shape
-------------  ---------  -----  -----  -----  -----  ----------  -------  --------------  ---------------------------------------------
shop.events    aggregate  53     5.6m   6.3s   8.9s   47,170,000  98889:1  COLLSCAN+SORT   {tenantId:eq, ts:gte} sort:{ts:-1}
shop.orders    find       89     2.4m   1.6s   2.4s   11,125,000  2976:1   COLLSCAN+SORT   {createdAt:gt, status:eq} sort:{createdAt:-1}
shop.sessions  find       58     34.7s  597ms  888ms  2,784,000   155:1    IXSCAN{active}  {active:eq, lastSeen:gt} sort:{lastSeen:-1}
shop.products  update     42     25.7s  612ms  799ms  2,268,000   54000:1  COLLSCAN        {sku:eq}
shop.users     find       56     6.7s   119ms  139ms  56          1:1      IXSCAN{email}   {email:eq}

cumMs  = total wall time accumulated across ALL occurrences (not one query)
docsEx = total documents examined across all occurrences
scan   = docsExamined per returned doc (high = index missing or weak)
&lt;/span&gt;&lt;span class="gp"&gt;plan   = most common query plan;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;COLLSCAN/+SORT &lt;span class="o"&gt;=&lt;/span&gt; index needed
&lt;span class="go"&gt;         empty plan (?) = plan not present in log (below slowms threshold)
&lt;/span&gt;&lt;span class="gp"&gt;next   : mdbkit advise &amp;lt;log&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;--ns&lt;/span&gt; &amp;lt;namespace&amp;gt;] &lt;span class="k"&gt;for &lt;/span&gt;index candidates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a screenshot I curated. &lt;code&gt;demo&lt;/code&gt; is seeded, so those two commands produce that exact table on your machine too, down to the last digit. It behaves the same on my laptop, on yours, and on a conference projector.&lt;/p&gt;

&lt;p&gt;The column that matters is the scan ratio. It is documents examined against documents returned, per query shape. A shape sitting at 98889:1 is reading roughly ninety nine thousand documents to hand back one. A shape at 1:1 is doing exactly the work it needs to.&lt;/p&gt;

&lt;p&gt;That single ratio is most of slow query triage. Everything else is detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow I actually use
&lt;/h2&gt;

&lt;p&gt;Three commands, in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, find the shape that is hurting.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mdbkit queries /var/log/mongodb/mongod.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slow operations grouped by query shape rather than listed individually. This matters more than it sounds. A log with 40,000 slow query lines is usually eight or nine distinct shapes repeating, and the one costing you the most is rarely the one that appears most often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then, ask what would fix it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mdbkit advise /var/log/mongodb/mongod.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This proposes candidate indexes based on the shapes it observed. More on how that works below, because it is the part people should be suspicious of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And when something has already gone wrong, build the timeline.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mdbkit triage /var/log/mongodb/mongod.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Elections, rollbacks, stalls, connection storms, in order, with timestamps. When you are twenty minutes into an incident and someone senior is asking what happened, a timeline you did not have to assemble by hand is worth a great deal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The log cannot tell you everything
&lt;/h2&gt;

&lt;p&gt;There is one failure mode a mongod log structurally cannot explain: the log simply stops, then starts again a minute later with no error in between.&lt;/p&gt;

&lt;p&gt;The process was killed before it could write anything. That answer lives in the system log, one line up from where you were looking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mdbkit triage mongod.log &lt;span class="nt"&gt;--oslog&lt;/span&gt; /var/log/syslog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CRIT] Process start(s) in window: mongod startup marker at 09:14:35
[CRIT] System: oom-kill: The kernel OOM killer terminated a task.
         1 occurrence (last at 09:14:03). Processes: mongod.
         next: This explains an unexplained restart in the log above.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;oslog&lt;/code&gt; reads &lt;code&gt;/var/log/syslog&lt;/code&gt; or &lt;code&gt;/var/log/messages&lt;/code&gt; and looks for the things that end database processes: OOM kills, file descriptor limits, segmentation faults, I/O errors, filesystems remounted read only, systemd service exits. Used with &lt;code&gt;triage&lt;/code&gt;, the unexplained restart gets matched to the kernel line that caused it.&lt;/p&gt;

&lt;p&gt;On journald systems there is no text log to read, and mdbkit will not run commands on your behalf, so it prints the &lt;code&gt;journalctl&lt;/code&gt; invocation for you to capture and feed back in.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;ftdc&lt;/code&gt;, which decodes &lt;code&gt;diagnostic.data&lt;/code&gt; offline. Every one of your nodes has been recording CPU, cache, queue depth and disk every second since the day you installed it, whether or not you run any monitoring. Plus &lt;code&gt;serverstatus&lt;/code&gt; for making sense of a &lt;code&gt;serverStatus&lt;/code&gt; dump, and &lt;code&gt;compare&lt;/code&gt; for answering whether the index you added last week actually helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  It never connects to your database
&lt;/h2&gt;

&lt;p&gt;No driver. No URI. No network code anywhere in it.&lt;/p&gt;

&lt;p&gt;mdbkit reads log files you hand it. It is strictly read only. Where an action would help, it prints the command for you to review and run yourself, rather than running anything on your behalf.&lt;/p&gt;

&lt;p&gt;I built it this way because I would not run a stranger's tool on a production host, and I do not expect you to either. So rather than asking you to trust me, the README shows you how to verify it independently. &lt;code&gt;grep&lt;/code&gt; the source for socket and driver imports. Run it under &lt;code&gt;strace&lt;/code&gt; and watch it make no network calls. It takes about a minute and you should do it.&lt;/p&gt;

&lt;p&gt;Zero runtime dependencies too, nothing in the supply chain beyond the Python standard library. That was a deliberate constraint and it cost me some convenience, but a tool that ships onto database servers should not drag a dependency tree behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The index advice is rules, not AI
&lt;/h2&gt;

&lt;p&gt;I gave a talk at the Dubai MongoDB User Group recently about how confidently language models describe a MongoDB that no longer exists. They will happily reference &lt;code&gt;plannerVersion&lt;/code&gt;, a field removed in 5.0. They do not know about EXPRESS stages in 8.0. And more importantly, they cannot know your cardinality, your write mix, or what is currently sitting in your plan cache, because none of that is in your query text.&lt;/p&gt;

&lt;p&gt;So mdbkit's index advice is deterministic. Rules over observed query shapes. The same log always produces the same recommendation.&lt;/p&gt;

&lt;p&gt;Every suggestion comes with the evidence it reasoned from, a confidence level, and how to validate it before you act. It says candidate, never command. And it will never tell you to drop an index, because deciding an index is unused requires knowing about traffic that a log file cannot show you.&lt;/p&gt;

&lt;p&gt;That is a deliberately narrow promise. I would rather it be right about a small thing than confident about a large one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would help most
&lt;/h2&gt;

&lt;p&gt;Two things, and they are both easy for you and hard for me.&lt;/p&gt;

&lt;p&gt;Real world log lines that parse incorrectly. Every MongoDB deployment logs slightly differently, and I have only seen the estates I have worked on. If mdbkit chokes on a line, that line is the most useful thing you can send me.&lt;/p&gt;

&lt;p&gt;Index advice that is wrong or unhelpful. If it suggests something you know is a bad idea for your workload, I want to know why, because that is a rule that needs fixing.&lt;/p&gt;

&lt;p&gt;Every release so far has come from someone doing exactly that. An FTDC decode that took twenty five minutes and pinned a CPU. A crash on a batched write. A restart with no explanation, which is why &lt;code&gt;oslog&lt;/code&gt; exists at all.&lt;/p&gt;

&lt;p&gt;Supports MongoDB 4.4 through 8.0. Python 3.8+.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/saqibameen86/mdbkit" rel="noopener noreferrer"&gt;https://github.com/saqibameen86/mdbkit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package: &lt;a href="https://pypi.org/project/mdbkit" rel="noopener noreferrer"&gt;https://pypi.org/project/mdbkit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free, MIT, and built because the gap annoyed me for long enough.&lt;/p&gt;




&lt;p&gt;If you liked this, I also wrote about &lt;a href="https://dev.to/saqibameen86/how-to-delete-2tb-from-a-live-mongodb-cluster-without-anyone-noticing-1pe2"&gt;deleting 2TB from a live MongoDB cluster without anyone noticing&lt;/a&gt; and &lt;a href="https://dev.to/saqibameen86/i-take-down-a-healthy-primary-on-purpose-you-should-too-bm7"&gt;why I take down a healthy primary on purpose&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>opensource</category>
      <category>database</category>
      <category>cli</category>
    </item>
    <item>
      <title>In Cassandra, a delete is a write (and reads pay for it)</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:36:09 +0000</pubDate>
      <link>https://dev.to/saqibameen86/in-cassandra-a-delete-is-a-write-and-reads-pay-for-it-5mb</link>
      <guid>https://dev.to/saqibameen86/in-cassandra-a-delete-is-a-write-and-reads-pay-for-it-5mb</guid>
      <description>&lt;p&gt;In Cassandra, a delete is a write.&lt;/p&gt;

&lt;p&gt;That sentence sounds like trivia until it takes down a read path. &lt;code&gt;DELETE&lt;/code&gt; doesn't remove anything. It &lt;em&gt;writes&lt;/em&gt; a &lt;strong&gt;tombstone&lt;/strong&gt;, a marker saying "this data is dead as of timestamp T." The actual removal happens later, during compaction, and only after a grace period. Between those two moments, your deleted data has negative value. It's gone from your application's point of view but still physically present, and every read that touches its range must process it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the marker has to exist
&lt;/h2&gt;

&lt;p&gt;Cassandra is a distributed, eventually consistent system. Suppose a delete simply removed data, and one replica was down when the delete happened. When it comes back, it still has the row, and to the rest of the cluster, that looks like &lt;em&gt;data the others are missing&lt;/em&gt;. Repair would helpfully copy the deleted row back to everyone.&lt;/p&gt;

&lt;p&gt;Deleted data returning from the dead is called &lt;strong&gt;zombie data&lt;/strong&gt;, and tombstones exist to prevent it. The tombstone outranks the older value everywhere it's seen.&lt;/p&gt;

&lt;p&gt;Which is why tombstones must survive for &lt;code&gt;gc_grace_seconds&lt;/code&gt;, &lt;strong&gt;default 864000, ten days&lt;/strong&gt;, before compaction may purge them. Ten days is the window you have to repair a down replica. Shrink &lt;code&gt;gc_grace_seconds&lt;/code&gt; without shrinking your repair interval and you've quietly signed up for zombies. Repair at scale is its own post, and it's coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  How reads pay
&lt;/h2&gt;

&lt;p&gt;A read merges data across memtable and SSTables. Every tombstone in the requested range must be read, held, and reconciled against live data before Cassandra can answer. A partition that has accumulated a million tombstones makes you &lt;em&gt;process a million dead cells to return whatever's alive&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Cassandra tells you when this is happening, in two escalating tones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN  ReadCommand - Read 812 live rows and 104,832 tombstone cells for query ...
ERROR ... Scanned over 100001 tombstones ... query aborted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The warning fires past &lt;code&gt;tombstone_warn_threshold&lt;/code&gt;, 1,000 by default. Past &lt;code&gt;tombstone_failure_threshold&lt;/code&gt;, 100,000, the read is killed mid flight with &lt;code&gt;TombstoneOverwhelmingException&lt;/code&gt;. That's the database choosing to fail your query rather than let it OOM the node.&lt;/p&gt;

&lt;p&gt;The thresholds are guardrails, not tuning knobs. Raising them treats the symptom and keeps the disease.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where tombstone farms come from
&lt;/h2&gt;

&lt;p&gt;Every one of these I've met in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue like workloads.&lt;/strong&gt; Insert, process, delete, repeat, in the same partition. The partition becomes a graveyard the consumer must scan past on every poll. Cassandra as a queue is the canonical anti pattern for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collection overwrites.&lt;/strong&gt; &lt;code&gt;UPDATE t SET mymap = {...}&lt;/code&gt; replaces a whole collection, which writes a range tombstone over the old one first. Prefer additive updates, &lt;code&gt;mymap = mymap + {...}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inserting NULLs.&lt;/strong&gt; Binding null in a prepared statement writes a tombstone for that cell. ORMs and "just bind every column" code generate these invisibly. Use unset values, not nulls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL everywhere.&lt;/strong&gt; Expired TTL cells become tombstones too. Fine &lt;em&gt;if&lt;/em&gt; your compaction strategy is built for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding them, then fixing them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# per-SSTable estimate&lt;/span&gt;
sstablemetadata /var/lib/cassandra/data/ks/table-&lt;span class="k"&gt;*&lt;/span&gt;/ &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-Data&lt;/span&gt;.db | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; droppable
&lt;span class="c"&gt;# Estimated droppable tombstones: 0.83&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.83 means an estimated 83% of that SSTable is purgeable tombstones.&lt;/p&gt;

&lt;p&gt;For query level visibility, &lt;code&gt;TRACING ON&lt;/code&gt; in cqlsh shows tombstone cells scanned per query. For a live table, &lt;code&gt;nodetool tablestats&lt;/code&gt; and watch average tombstones per slice.&lt;/p&gt;

&lt;p&gt;The durable fixes are modeling fixes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Time series with TTL, use TimeWindowCompactionStrategy.&lt;/strong&gt; Whole SSTables age out and get dropped as files, so tombstone processing largely disappears. Compaction strategy trade offs are the next post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop deleting in place in hot partitions.&lt;/strong&gt; Partition by time bucket and let whole partitions expire instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit for null binding and collection overwrites.&lt;/strong&gt; These are the tombstones nobody meant to write.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deletes in Cassandra are cheap to issue and expensive to have issued. Model as if every delete is a small loan against your read latency, because it is, and &lt;code&gt;gc_grace_seconds&lt;/code&gt; is the repayment schedule.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>database</category>
      <category>performance</category>
      <category>distributed</category>
    </item>
    <item>
      <title>How to delete 2TB from a live MongoDB cluster without anyone noticing</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:04:47 +0000</pubDate>
      <link>https://dev.to/saqibameen86/how-to-delete-2tb-from-a-live-mongodb-cluster-without-anyone-noticing-1pe2</link>
      <guid>https://dev.to/saqibameen86/how-to-delete-2tb-from-a-live-mongodb-cluster-without-anyone-noticing-1pe2</guid>
      <description>&lt;p&gt;The request was simple. "There's about 2TB of expired data in this collection. Can you delete it tonight?"&lt;/p&gt;

&lt;p&gt;The honest answer was no, and being able to explain &lt;em&gt;why&lt;/em&gt;, not just refuse, is most of the job. A missing TTL index had let short lived data pile up for months in one of our busiest collections. The team wanted one big &lt;code&gt;deleteMany&lt;/code&gt; overnight. Here's what that would have actually done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a mass delete hurts, and it isn't locking
&lt;/h2&gt;

&lt;p&gt;People reach for "locks" as the explanation. Wrong database era. WiredTiger uses document level concurrency, so a huge delete doesn't freeze the collection. The damage arrives through three quieter channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The oplog becomes a firehose.&lt;/strong&gt; Every deleted document is an individual entry in the oplog. Delete 500 million documents and you've written 500 million oplog entries, which every secondary must pull and apply. Replication lag climbs. If lag exceeds what the oplog window can hold, a secondary falls off the oplog entirely and needs a &lt;strong&gt;full resync&lt;/strong&gt;. Now your delete has cost you redundancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cache churn.&lt;/strong&gt; To delete a document, WiredTiger loads its pages into cache. A 2TB sweep drags cold data through a cache that was carefully full of hot data. Your working set gets evicted, p99 latency on unrelated queries climbs, and someone opens an incident that will never mention the word "delete."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Acknowledgment pressure.&lt;/strong&gt; If your writes use &lt;code&gt;w:"majority"&lt;/code&gt; (they should, see the write concern post), the delete's progress is gated on those same struggling secondaries. Everything compounds.&lt;/p&gt;

&lt;p&gt;I demonstrated exactly this in a lower environment. Kicked off the naive delete, watched &lt;code&gt;rs.printSecondaryReplicationInfo()&lt;/code&gt; lag grow, and the "tonight" deadline converted itself into a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: chunk, sleep, watch the lag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SLEEP_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SLEEP_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// let the oplog breathe&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; deleted, lag check...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two operational rules around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run in off peak windows only.&lt;/strong&gt; We ran nightly windows and paused at business hours. Duration was about a week and a half for the full 2TB. Nobody noticed, which was the definition of success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch replication lag as your throttle.&lt;/strong&gt; If lag climbs past your comfort line, raise &lt;code&gt;SLEEP_MS&lt;/code&gt;. The loop's speed limit is the cluster's health, not your patience.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sleep()&lt;/code&gt; between chunks looks unbearably conservative to developers. That is the point. The delete has no deadline once the growth is stopped, and the growth is stopped by the &lt;em&gt;real&lt;/em&gt; fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real fix: TTL, so this never becomes a project again
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expireAfterSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2592000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// 30 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Details worth knowing about the TTL monitor, because they surprise people.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It wakes &lt;strong&gt;every 60 seconds&lt;/strong&gt;, so expiry is approximate, not instant. Fine for cleanup, wrong tool for business logic that needs precise expiry.&lt;/li&gt;
&lt;li&gt;It deletes in &lt;strong&gt;background batches&lt;/strong&gt;, effectively running my chunked loop forever, gently. On huge backlogs it can lag, which is why we cleared the 2TB manually first and let TTL own steady state.&lt;/li&gt;
&lt;li&gt;Index the field the data actually ages by. TTL on the wrong date field is a slow motion data loss incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We closed it with a runbook, the chunk script, the lag thresholds and the window schedule, so the next 2TB request is a lookup, not a debate.&lt;/p&gt;

&lt;p&gt;The fastest way to delete 2TB is slowly.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>operations</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>I take down a healthy primary on purpose. You should too.</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:58:41 +0000</pubDate>
      <link>https://dev.to/saqibameen86/i-take-down-a-healthy-primary-on-purpose-you-should-too-bm7</link>
      <guid>https://dev.to/saqibameen86/i-take-down-a-healthy-primary-on-purpose-you-should-too-bm7</guid>
      <description>&lt;p&gt;At one company I built an autoscaling pipeline that replaced MongoDB cluster nodes automatically. CloudWatch caught the CPU threshold, and the automation rolled the cluster node by node. Secondary first, second secondary next, primary last. Twelve minutes, no human, no application impact.&lt;/p&gt;

&lt;p&gt;The order is the interesting part. Rolling the primary last means the cluster experiences exactly &lt;strong&gt;one&lt;/strong&gt; election per cycle, at a moment the automation chooses. Which only works if you trust elections. And you only get to trust elections by triggering them yourself, regularly, and watching what your application does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens in an election
&lt;/h2&gt;

&lt;p&gt;The mechanics, compressed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every node heartbeats every 2 seconds. A node that misses heartbeats for &lt;code&gt;electionTimeoutMillis&lt;/code&gt; (default &lt;strong&gt;10 seconds&lt;/strong&gt;) is presumed gone.&lt;/li&gt;
&lt;li&gt;An electable secondary calls an election. Voting is not seniority. A node only wins if its oplog is at least as recent as each voter's. &lt;strong&gt;Freshness outranks priority.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority&lt;/code&gt; settings bias who &lt;em&gt;stands&lt;/em&gt; for election, not who deserves to win. A priority 10 node with a stale oplog loses to a priority 1 node that's caught up.&lt;/li&gt;
&lt;li&gt;Majority vote, new primary, secondaries sync from it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wall clock cost is typically a few seconds for the election itself, bounded by that 10 second detection window on the front. Call it 5 to 15 seconds of no primary, during which writes cannot be acknowledged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that pages you isn't the election
&lt;/h2&gt;

&lt;p&gt;It's what your application does during those seconds. Three behaviours, in ascending order of maturity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Errors bubble to users.&lt;/strong&gt; Driver throws &lt;code&gt;NotWritablePrimary&lt;/code&gt;, nobody catches it, checkout fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind retry loops.&lt;/strong&gt; Sometimes double applies the write. Worse than failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retryWrites=true&lt;/code&gt; plus a sane &lt;code&gt;serverSelectionTimeoutMS&lt;/code&gt;.&lt;/strong&gt; The driver holds the write, discovers the new primary from the topology update, retries exactly once. To the user, one slow request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not find out which of these you are during a real incident. That's the wrong time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The drill
&lt;/h2&gt;

&lt;p&gt;Quarterly. In staging first, then production during a low traffic window. Yes, production. Staging tells you the drill works, production tells you the truth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Note current topology&lt;/span&gt;
&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateStr&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Ask the primary to step down gracefully&lt;/span&gt;
&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stepDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// refuses re-election for 60s&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Watch the election from a secondary&lt;/span&gt;
&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While it runs, the numbers I capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detection to new primary elected : ___ s
application error count          : ___
p99 latency during window        : ___ ms
first write after failover       : double applied? (check by unique key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those blanks are the deliverable. Fill them from your own drill.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rs.stepDown()&lt;/code&gt; is the polite version. The primary flushes and closes cleanly. Once that's boring, graduate to the impolite version. &lt;code&gt;kill -9&lt;/code&gt; the primary's &lt;code&gt;mongod&lt;/code&gt;, or drop its network. That's the one that resembles an actual cloud incident, and it exercises the full 10 second detection window instead of skipping it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two configuration notes from doing this at scale
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Even numbered voting members are a bug.&lt;/strong&gt; Four voters can split 2 to 2 and elect nobody. Keep voting members odd. Use an arbiter only if you truly can't afford a third data node, and know that arbiters don't help &lt;code&gt;w:"majority"&lt;/code&gt; acknowledge anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priorities encode your topology intent.&lt;/strong&gt; In multi region clusters we set higher priorities in the primary region so failback happens automatically once nodes recover. But again, priority only nominates. The oplog decides.&lt;/p&gt;

&lt;p&gt;An election you've rehearsed is an operational event. An election you haven't is an outage with paperwork.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>highavailability</category>
      <category>sre</category>
      <category>database</category>
    </item>
    <item>
      <title>"The write was acknowledged" means less than you think</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:49:20 +0000</pubDate>
      <link>https://dev.to/saqibameen86/the-write-was-acknowledged-means-less-than-you-think-4nm8</link>
      <guid>https://dev.to/saqibameen86/the-write-was-acknowledged-means-less-than-you-think-4nm8</guid>
      <description>&lt;p&gt;I spent years operating databases for payment platforms, where a lost write isn't a bug, it's a regulatory conversation. That environment teaches you to read the phrase "the write was acknowledged" the way a lawyer reads a contract. Acknowledged &lt;em&gt;by whom&lt;/em&gt;? Durable &lt;em&gt;against what&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;In MongoDB, that one sentence has at least four different meanings, controlled by two settings most applications leave at defaults without ever deciding to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four contracts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. w:1, the primary has it in memory&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writeConcern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 2. w:1, j:true, the primary has it in its journal on disk&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writeConcern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;j&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 3. w:"majority", a majority of the replica set has it&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writeConcern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;majority&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 4. w:"majority", j:true, a majority has it journaled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each level survives a different failure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;w:1&lt;/strong&gt; survives nothing interesting. If the primary's &lt;code&gt;mongod&lt;/code&gt; crashes before the next journal flush, the write is gone, while your application already told the user "success."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;w:1, j:true&lt;/strong&gt; survives a process crash on the primary. It does not survive the primary &lt;em&gt;failing over&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;w:"majority"&lt;/strong&gt; survives failover. This is the one that matters, and here's why.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where acknowledged writes go to die
&lt;/h2&gt;

&lt;p&gt;A replica set election isn't polite. If the primary accepts writes at &lt;code&gt;w:1&lt;/code&gt;, then loses connectivity before replicating them, a secondary gets elected and moves on. When the old primary rejoins, it discovers history diverged. It has writes the new primary never saw.&lt;/p&gt;

&lt;p&gt;Those writes get &lt;strong&gt;rolled back&lt;/strong&gt;. MongoDB doesn't delete them silently, it writes them to BSON files in the &lt;code&gt;rollback&lt;/code&gt; directory, where, in my experience, they are examined by precisely no one until an auditor asks a question.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;w:"majority"&lt;/code&gt; closes this hole. The write isn't acknowledged until enough nodes have it that &lt;em&gt;any electable primary&lt;/em&gt; must have it too. It cannot be elected away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The read side of the same contract
&lt;/h2&gt;

&lt;p&gt;Durable writes with careless reads is half a system. Two settings complete it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't show me data that could still be rolled back&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({...}).&lt;/span&gt;&lt;span class="nf"&gt;readConcern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;majority&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// My own session should see its own writes, in order&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;causalConsistency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the driver, &lt;code&gt;retryWrites=true&lt;/code&gt; (default in modern drivers) makes the failover window survivable. A write interrupted by an election is retried exactly once against the new primary, safely, because every write carries a unique transaction number.&lt;/p&gt;

&lt;p&gt;Which is also why "just wrap it in a retry loop yourself" is worse than the built in one. Your loop can double apply. The driver's cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually deploy
&lt;/h2&gt;

&lt;p&gt;Tiering, not one global setting.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Write concern&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payments, ledger, anything auditable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w:"majority"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rollback is not an acceptable word here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User profile updates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w:"majority"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cheap insurance, users notice lost edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High volume telemetry, click events&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Losing two seconds of events in a rare failover is a fair trade for throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The latency cost of majority is one replication round trip, single digit milliseconds inside a region. I've watched teams run payments at &lt;code&gt;w:1&lt;/code&gt; to save five milliseconds, which is a way of saying they priced a lost financial record at five milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab
&lt;/h2&gt;

&lt;p&gt;Start a 3 node replica set locally (&lt;code&gt;mongod --replSet&lt;/code&gt; three times, or docker), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// terminal 1: insert at w:1 in a loop&lt;/span&gt;
&lt;span class="c1"&gt;// terminal 2: rs.stepDown() on the primary mid-loop&lt;/span&gt;
&lt;span class="c1"&gt;// then check the old primary's rollback directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /data/rs1/rollback/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it. Seeing your own acknowledged write sitting in a rollback file is worth more than this whole post.&lt;/p&gt;

&lt;p&gt;Defaults are decisions someone else made. For write concern, make your own, per collection, on purpose, written down.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>database</category>
      <category>reliability</category>
      <category>distributed</category>
    </item>
    <item>
      <title>cannot index parallel arrays, the MongoDB error that improves your schema</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:41:53 +0000</pubDate>
      <link>https://dev.to/saqibameen86/cannot-index-parallel-arrays-the-mongodb-error-that-improves-your-schema-4pp</link>
      <guid>https://dev.to/saqibameen86/cannot-index-parallel-arrays-the-mongodb-error-that-improves-your-schema-4pp</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MongoServerError: cannot index parallel arrays [certifications] [skills]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've never hit this error, your schemas have been lucky. A team once brought me a document shaped like this during a design review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engineer-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cassandra&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;python&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;certifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They wanted fast queries on skill and certification combinations, so the natural move was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engineers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;certifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// MongoServerError: cannot index parallel arrays&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MongoDB refuses. Not "performs badly", refuses. And the reason is one of those internals that, once you see it, changes how you model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: multikey indexes are per element
&lt;/h2&gt;

&lt;p&gt;When you index an array field, MongoDB creates a &lt;strong&gt;multikey index&lt;/strong&gt;, one index entry per array element. Index &lt;code&gt;skills&lt;/code&gt; on the document above and you get three entries, one for &lt;code&gt;"mongodb"&lt;/code&gt;, one for &lt;code&gt;"cassandra"&lt;/code&gt;, one for &lt;code&gt;"python"&lt;/code&gt;, each pointing at the same document.&lt;/p&gt;

&lt;p&gt;Now imagine a compound index across two independent arrays. To answer any combination query, MongoDB would need an entry for every element of the &lt;em&gt;cross product&lt;/em&gt;. Three skills times two certifications is six entries. Harmless here. But two arrays of 1,000 elements each is a &lt;strong&gt;million index entries for a single document&lt;/strong&gt;. One &lt;code&gt;insertOne&lt;/code&gt; fans out into a million B-tree writes. The server declines to let you build that footgun, and honestly, good.&lt;/p&gt;

&lt;p&gt;There's a quieter problem hiding in the schema too. The combinations are meaningless. Nothing in two parallel arrays says &lt;em&gt;which&lt;/em&gt; skill relates to &lt;em&gt;which&lt;/em&gt; certification. The data model can't answer the question the index was supposed to serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one array of subdocuments
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engineer-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;quals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cassandra&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engineers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quals.skill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quals.cert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is allowed because both indexed paths traverse the &lt;strong&gt;same&lt;/strong&gt; array. Index entries are generated per array element, so each entry is a real &lt;code&gt;(skill, cert)&lt;/code&gt; pair that actually existed together in the data, the correlation the parallel arrays destroyed. The index gets smaller and the model gets more truthful. That combination is rare.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing before you rely on it. To match both fields &lt;em&gt;within the same element&lt;/em&gt;, query with &lt;code&gt;$elemMatch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engineers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$elemMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;$elemMatch&lt;/code&gt;, MongoDB matches documents where &lt;em&gt;some&lt;/em&gt; element has the skill and &lt;em&gt;some other&lt;/em&gt; element has the cert. The parallel arrays bug, reincarnated at query time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;                          &lt;span class="c1"&gt;// watch it fail&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pairs.a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pairs.b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;         &lt;span class="c1"&gt;// watch it work&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$elemMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// zero results, and that's correct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the whole lesson in one query. &lt;code&gt;a:1&lt;/code&gt; and &lt;code&gt;b:"y"&lt;/code&gt; never occurred in the same element, so the document doesn't match. Parallel arrays would have happily lied to you.&lt;/p&gt;

&lt;p&gt;Some errors are the database being difficult. This one is the database refusing to let your schema make a promise it can't keep.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>indexing</category>
      <category>datamodeling</category>
      <category>database</category>
    </item>
    <item>
      <title>The explain plan I read before the CEO called back!</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Tue, 04 Aug 2026 05:09:05 +0000</pubDate>
      <link>https://dev.to/saqibameen86/the-explain-plan-i-read-before-the-ceo-called-back-1kla</link>
      <guid>https://dev.to/saqibameen86/the-explain-plan-i-read-before-the-ceo-called-back-1kla</guid>
      <description>&lt;p&gt;The escalation reached our CEO before it reached my team.&lt;/p&gt;

&lt;p&gt;An enterprise customer, one of the largest on the platform, was seeing latency spikes bad enough that they raised it commercially, not technically. I had about twenty minutes before a call with their leadership, so I did the only thing that ever works in that situation: I stopped guessing and pulled the profiler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProfilingLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slowms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// ...wait one cycle...&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;millis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set the threshold (1000 ms) high on purpose. The default log already captures slow queries over 100 ms, but the profiler gives me something the log doesn't: structured records I can sort and aggregate. I want it catching only the genuinely pathological queries, so the culprit stands out and the profiler itself stays light on an already stressed system.&lt;/p&gt;

&lt;p&gt;One aggregation stood out. It ran every five minutes, scanned a collection of roughly 200 million documents, and had no index supporting it. Every five minutes, a full collection scan, on shared infrastructure. The "noisy neighbor" wasn't a mystery, it was on a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually read an explain plan
&lt;/h2&gt;

&lt;p&gt;Run the slow query with execution stats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executionStats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ACME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I look at exactly three numbers before anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nReturned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;214&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;totalKeysExamined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;totalDocsExamined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;198236411&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ratio is the diagnosis. Healthy queries examine roughly as many keys as they return. This one examined 198 million documents to return 214. &lt;code&gt;totalKeysExamined: 0&lt;/code&gt; means no index was touched at all. The winning plan was &lt;code&gt;COLLSCAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the whole skill, honestly. Everything else in the explain output is detail. If &lt;code&gt;totalDocsExamined&lt;/code&gt; is orders of magnitude above &lt;code&gt;nReturned&lt;/code&gt;, the query is doing the database's job by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  ESR: the field order that makes or breaks the index
&lt;/h2&gt;

&lt;p&gt;The fix was not "add an index." It was "add the right index" and the difference is field order. MongoDB compound indexes follow what the docs call the ESR rule, Equality, Sort, Range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Equality&lt;/strong&gt; predicates first (&lt;code&gt;accountId&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; fields next (&lt;code&gt;created&lt;/code&gt; as sort key)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range&lt;/strong&gt; predicates last (&lt;code&gt;created: { $gte: ... }&lt;/code&gt;, here sort and range share a field, which is the friendly case)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get the order wrong, range before sort, and the index still gets used, but MongoDB has to fetch and sort in memory. You'll see it in the plan as a &lt;code&gt;SORT&lt;/code&gt; stage instead of the sort being absorbed by index order. On 200 million documents, an in-memory sort is not a detail.&lt;/p&gt;

&lt;p&gt;One refinement we used because the query only ever cared about active records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;partialFilterExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partial index keeps only the entries that match the filter. Smaller index, less RAM, faster writes, and the query planner picks it up as long as the query includes the same predicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab, if you want to see it yourself
&lt;/h2&gt;

&lt;p&gt;Five minutes on a local &lt;code&gt;mongod&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;997&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executionStats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// note totalDocsExamined, then:&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// run the explain again and compare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The before and after on &lt;code&gt;totalDocsExamined&lt;/code&gt; makes the argument better than any slide deck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened with the customer
&lt;/h2&gt;

&lt;p&gt;Root cause explained in plain language on the call. I described the collection as a library with no catalogue, where every request meant walking every aisle. Partial index shipped the same day. Latency resolved within six hours of the escalation, and two weeks later I presented them a health report instead of an apology. They renewed.&lt;/p&gt;

&lt;p&gt;Twenty minutes, the profiler, and three numbers. The hard part was knowing which three.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>performance</category>
      <category>indexing</category>
      <category>database</category>
    </item>
  </channel>
</rss>
