<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Distributed-Systems on Krishnatejaswi S</title>
    <link>https://KTS-o7.github.io/bear/tags/distributed-systems/</link>
    <description>Recent content in Distributed-Systems on Krishnatejaswi S</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</managingEditor>
    <webMaster>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</webMaster>
    <copyright>Krishnatejaswi S</copyright>
    <lastBuildDate>Sun, 03 May 2026 00:00:00 +0530</lastBuildDate>
    <atom:link href="https://KTS-o7.github.io/bear/tags/distributed-systems/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>CRDTs: Conflict-Free Replicated Data Types</title>
      <link>https://KTS-o7.github.io/bear/systems/crdts/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/crdts/</guid>
      <description>&lt;p&gt;Vector clocks tell you when two writes conflict. They don&amp;rsquo;t tell you what to do about it. That&amp;rsquo;s still your problem.&lt;/p&gt;&#xA;&lt;p&gt;CRDTs take a different position: design the data structure so that conflicts can never happen. Not &amp;ldquo;detect and resolve&amp;rdquo; &amp;ndash; just structurally eliminate the problem.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-core-idea&#34;&gt;The Core Idea&lt;/h2&gt;&#xA;&lt;p&gt;A CRDT is a data structure with a merge function that is commutative, associative, and idempotent. Those three properties together mean you can apply updates in any order, any number of times, from any subset of nodes, and always converge to the same state.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Vector clocks tell you when two writes conflict. They don&rsquo;t tell you what to do about it. That&rsquo;s still your problem.</p>
<p>CRDTs take a different position: design the data structure so that conflicts can never happen. Not &ldquo;detect and resolve&rdquo; &ndash; just structurally eliminate the problem.</p>
<h2 id="the-core-idea">The Core Idea</h2>
<p>A CRDT is a data structure with a merge function that is commutative, associative, and idempotent. Those three properties together mean you can apply updates in any order, any number of times, from any subset of nodes, and always converge to the same state.</p>
<ul>
<li><strong>Commutative</strong>: merge(A, B) = merge(B, A) &ndash; order doesn&rsquo;t matter</li>
<li><strong>Associative</strong>: merge(A, merge(B, C)) = merge(merge(A, B), C) &ndash; grouping doesn&rsquo;t matter</li>
<li><strong>Idempotent</strong>: merge(A, A) = A &ndash; applying the same update twice is harmless</li>
</ul>
<p>If your data structure satisfies these, you can replicate it across nodes with no coordination. Each node applies updates locally and gossips state to peers. Eventually everyone has the same state.</p>
<h2 id="the-simplest-crdt-g-counter">The Simplest CRDT: G-Counter</h2>
<p>A grow-only counter. Each node maintains a vector of counts, one slot per node. To increment, a node adds to its own slot. To read the total, sum all slots. To merge two replicas, take the element-wise max.</p>





<pre tabindex="0"><code>Node A: [3, 1, 0]   Node B: [2, 2, 0]
Merge:  [3, 2, 0]   -- take max per slot
Total:  5</code></pre><p>Node A can never decrement B&rsquo;s slot and Node B can never decrement A&rsquo;s slot. Merges always move counters up, never down. Convergence is guaranteed.</p>
<h2 id="adding-decrements-pn-counter">Adding Decrements: PN-Counter</h2>
<p>A grow-only counter can&rsquo;t go down. If you need decrements, use two G-Counters &ndash; one for increments (P), one for decrements (N). The value is P - N.</p>
<p>Each node increments its P slot to add and its N slot to subtract. Merges take the element-wise max of both vectors. The final value is the sum of all P slots minus the sum of all N slots.</p>
<h2 id="sets-the-interesting-case">Sets: The Interesting Case</h2>
<p>Sets are where CRDTs get non-obvious. The naive approach breaks immediately: if Node A removes an element at the same time Node B adds it, what&rsquo;s the right answer?</p>
<p><strong>G-Set</strong> (grow-only set) sidesteps this: elements can only be added, never removed. Merge is union. Works perfectly but is obviously limited.</p>
<p><strong>2P-Set</strong> (two-phase set) adds a remove set R alongside the add set A. An element is in the set if it&rsquo;s in A but not R. Once removed, it can never be re-added. Merge is union on both sets.</p>
<p><strong>LWW-Element-Set</strong> (last-write-wins) attaches a timestamp to each add and remove. On conflict, the higher timestamp wins. Simple, but you&rsquo;re back to trusting clocks.</p>
<p><strong>OR-Set</strong> (observed-remove set) is the one that gets this right. Each add operation generates a unique tag. Remove only removes the specific tags that were observed at remove time. If Node A adds element X (tag: t1) and Node B concurrently adds X again (tag: t2), then Node A removing X (removing tag t1) leaves Node B&rsquo;s addition (tag t2) intact. The element stays in the set.</p>
<p>This matches user intuition: you removed the copy you knew about, not the one that was being added concurrently somewhere else.</p>
<h2 id="state-based-vs-operation-based">State-based vs. Operation-based</h2>
<p>There are two families of CRDTs.</p>
<p><strong>State-based (CvRDTs)</strong>: nodes gossip their full state. Merge is applied on receipt. The merge function must be a join on a join-semilattice &ndash; a partial order where every pair of elements has a least upper bound. Simple to reason about, but shipping full state can be expensive for large structures.</p>
<p><strong>Operation-based (CmRDTs)</strong>: nodes broadcast operations instead of state. No merge function needed on state &ndash; instead, the transport layer must guarantee exactly-once delivery and causal ordering. More efficient, but the delivery guarantee is a real constraint.</p>
<p>In practice, libraries like Automerge and Yjs use operation-based CRDTs with a hybrid approach: operations for real-time sync, periodic state snapshots for catch-up after disconnection.</p>
<h2 id="where-theyre-used">Where They&rsquo;re Used</h2>
<p><strong>Collaborative editing</strong> is the obvious one. Google Docs uses OT (operational transformation), an older approach with similar goals. Figma uses CRDTs. Notion, Linear, and most newer collaborative tools have moved toward CRDTs because the math is cleaner and they handle offline edits naturally.</p>
<p><strong>Distributed databases</strong>: Riak&rsquo;s eventual consistency model was built around CRDTs. Redis has CRDT support in Redis Enterprise for multi-region deployments. Azure Cosmos DB uses them internally for its multi-master replication.</p>
<p><strong>Shopping carts</strong>: Amazon&rsquo;s Dynamo paper mentions the shopping cart as a canonical use case. Adds and removes to a cart from multiple devices or regions converge without a coordinator.</p>
<h2 id="the-tradeoff">The Tradeoff</h2>
<p>CRDTs don&rsquo;t work for everything. The merge semantics have to match what your application actually means.</p>
<p>A bank account is not a PN-Counter. If your account shows $500 and you withdraw $200 concurrently from two ATMs, merging the two PN-Counters would show $100 &ndash; which is correct math but incorrect banking. The constraint &ldquo;balance must not go negative&rdquo; requires coordination. You can&rsquo;t avoid that with a CRDT.</p>
<p>The rule: CRDTs work when the merge semantics match application intent and when the application can tolerate the conflict-free resolution behavior. They&rsquo;re not a universal replacement for coordination &ndash; they&rsquo;re a way to avoid coordination in cases where the math lines up.</p>
<p>Vector clocks give you the diagnosis. CRDTs give you a structural guarantee that the diagnosis is never needed.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How Spanner Does Distributed Transactions Without Classic 2PC</title>
      <link>https://KTS-o7.github.io/bear/systems/spanner_distributed_transactions/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/spanner_distributed_transactions/</guid>
      <description>&lt;p&gt;Two-phase commit has a well-known problem: crash the coordinator between Phase 1 and Phase 2 and every participant is stuck holding locks, waiting, with no safe way to proceed. 2PC is a blocking protocol.&lt;/p&gt;&#xA;&lt;p&gt;Google Spanner runs distributed transactions across a globally replicated database at scale. It can&amp;rsquo;t afford to block on coordinator failure. Its solution is to not use a standalone coordinator at all &amp;ndash; it runs 2PC on top of Paxos groups, which means the coordinator itself is fault-tolerant.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Two-phase commit has a well-known problem: crash the coordinator between Phase 1 and Phase 2 and every participant is stuck holding locks, waiting, with no safe way to proceed. 2PC is a blocking protocol.</p>
<p>Google Spanner runs distributed transactions across a globally replicated database at scale. It can&rsquo;t afford to block on coordinator failure. Its solution is to not use a standalone coordinator at all &ndash; it runs 2PC on top of Paxos groups, which means the coordinator itself is fault-tolerant.</p>
<h2 id="the-core-move-paxos-groups-as-participants">The Core Move: Paxos Groups as Participants</h2>
<p>In classic 2PC, each participant is a single node. It votes, acquires locks, and waits. If it crashes, the transaction is stuck.</p>
<p>Spanner&rsquo;s participants are Paxos groups. Each shard of data is managed by a Paxos group &ndash; typically 5 replicas spread across datacenters. The Paxos leader for each group acts as the 2PC participant. Lock acquisition and voting happen on the leader, and the decision is durably replicated to the group before the leader responds.</p>
<p>If the coordinator crashes, a new coordinator can be elected (it&rsquo;s also a Paxos group). If a participant leader crashes, a new leader in that group takes over &ndash; the lock state and vote are already replicated, so the protocol can continue.</p>
<p>The blocking problem is solved not by changing the protocol but by making every node in the protocol fault-tolerant.</p>
<h2 id="truetime">TrueTime</h2>
<p>Spanner adds something 2PC doesn&rsquo;t have: globally synchronized timestamps, accurate enough to use for transaction ordering.</p>
<p>Google runs GPS receivers and atomic clocks in each datacenter and exposes a <code>TrueTime</code> API that returns a time interval <code>[earliest, latest]</code> rather than a single timestamp. The interval accounts for clock uncertainty. The guarantee is that the real current time lies somewhere in that interval.</p>
<p>Spanner uses TrueTime to assign commit timestamps. The rule: a transaction&rsquo;s commit timestamp must be after the <code>latest</code> bound of TrueTime at the moment the coordinator decides to commit. This means Spanner waits for a brief period &ndash; typically 1-7ms &ndash; before declaring a transaction committed, to ensure that any other transaction starting after this one will see a strictly later timestamp.</p>
<p>This is the &ldquo;commit wait.&rdquo; It&rsquo;s intentional latency, paid to guarantee that commit timestamps accurately reflect real-world ordering.</p>
<h2 id="external-consistency">External Consistency</h2>
<p>The property this buys is external consistency: if transaction T1 commits before T2 starts (in real-world time), T2 will see T1&rsquo;s writes. This is stronger than serializability &ndash; serializable transactions can be ordered in any way consistent with their conflicts, even if that reorders real-world causality. External consistency preserves real-world order.</p>
<p>In practical terms: if you commit a write and then tell a colleague &ldquo;it&rsquo;s committed, go read it,&rdquo; their read will see your write. There&rsquo;s no window where a causally later transaction misses a causally earlier committed write.</p>
<p>Most distributed databases don&rsquo;t provide this. They provide serializability, which is strong, but allows committed writes to be temporarily invisible to transactions that started after the commit in real time. Spanner&rsquo;s external consistency closes that gap.</p>
<h2 id="what-the-2pc-layer-actually-looks-like">What the 2PC Layer Actually Looks Like</h2>
<p>A read-write transaction in Spanner:</p>
<ol>
<li>Client reads data, buffers writes locally (no locks yet)</li>
<li>Client sends commit request to the coordinator Paxos group</li>
<li>Coordinator runs 2PC with each participant Paxos group:
<ul>
<li>Sends <code>PREPARE</code> to each participant leader</li>
<li>Each participant leader acquires locks, logs the prepare record to its Paxos group, replies <code>YES</code></li>
<li>Coordinator picks a commit timestamp (TrueTime <code>latest</code> + epsilon), logs it to its group</li>
<li>Sends <code>COMMIT</code> with the timestamp to all participants</li>
</ul>
</li>
<li>Coordinator waits out the commit wait interval</li>
<li>Participants apply the writes at the commit timestamp, release locks</li>
<li>Client sees commit confirmation</li>
</ol>
<p>The 2PC messages are still there. The key difference is that every step that touches durable state goes through a Paxos replication round first. The coordinator can crash and be replaced. A participant leader can crash and its successor has the full prepare state. Nothing blocks permanently.</p>
<h2 id="the-cost">The Cost</h2>
<p>The commit wait adds latency. For single-region transactions it&rsquo;s small (sub-millisecond when clocks are well-synchronized). For multi-region transactions where the coordinator and participants are in different continents, round-trip latency dominates and the TrueTime uncertainty is larger &ndash; you&rsquo;re looking at tens of milliseconds.</p>
<p>Spanner is designed for this tradeoff: strong global consistency in exchange for latency that scales with geographic distance. For financial systems, global inventory, or anything that requires real external consistency, that tradeoff is often correct. For low-latency local transactions, it&rsquo;s usually not.</p>
<p>Classic 2PC is blocking because the coordinator is a single node. Spanner&rsquo;s answer is not to fix 2PC &ndash; it&rsquo;s to make every node in the 2PC protocol a replicated state machine.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Multi-Paxos: From Single Decree to a Replicated Log</title>
      <link>https://KTS-o7.github.io/bear/systems/multi_paxos/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/multi_paxos/</guid>
      <description>&lt;p&gt;Basic Paxos agrees on a single value. That&amp;rsquo;s useful for electing a leader or picking a configuration, but a database needs to agree on a sequence of values &amp;ndash; every write in order, forever. Running one full round of Paxos per log slot would work but it&amp;rsquo;s expensive: two network round-trips per slot, and Phase 1 has to complete before Phase 2 can start.&lt;/p&gt;&#xA;&lt;p&gt;Multi-Paxos is the optimization that makes Paxos practical as a replicated log. The insight is that Phase 1 can be amortized across many slots once you have a stable leader.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Basic Paxos agrees on a single value. That&rsquo;s useful for electing a leader or picking a configuration, but a database needs to agree on a sequence of values &ndash; every write in order, forever. Running one full round of Paxos per log slot would work but it&rsquo;s expensive: two network round-trips per slot, and Phase 1 has to complete before Phase 2 can start.</p>
<p>Multi-Paxos is the optimization that makes Paxos practical as a replicated log. The insight is that Phase 1 can be amortized across many slots once you have a stable leader.</p>
<h2 id="what-phase-1-actually-does">What Phase 1 Actually Does</h2>
<p>Recall basic Paxos. Phase 1 (Prepare/Promise) establishes that a proposer has the highest ballot number any acceptor will respond to. The acceptors promise to ignore any lower-numbered proposer. This is the &ldquo;take control&rdquo; phase &ndash; after it completes, the proposer has permission to drive proposals through Phase 2.</p>
<p>Phase 1 doesn&rsquo;t say anything about which value gets proposed. It just establishes leadership over the acceptors for a given ballot number.</p>
<h2 id="the-amortization">The Amortization</h2>
<p>If the same leader runs Phase 1 once with a high ballot number, it doesn&rsquo;t need to run Phase 1 again for subsequent log slots &ndash; as long as no other proposer challenges it with a higher ballot.</p>
<p>The leader runs Phase 1 once, gets promises from a majority, and then drives Phase 2 directly for slot 1, slot 2, slot 3, and so on. Each slot is just one round-trip (Accept/Accepted) instead of two. Throughput roughly doubles for steady-state operation under a stable leader.</p>





<pre tabindex="0"><code>Phase 1 (once):   Leader → Prepare(ballot=5) → Majority
                  Majority → Promise(ballot=5, ...) → Leader

Phase 2 (per slot):
  Slot 1:  Leader → Accept(ballot=5, slot=1, value=A) → Majority
           Majority → Accepted → Leader → Committed
  Slot 2:  Leader → Accept(ballot=5, slot=2, value=B) → Majority
           Majority → Accepted → Leader → Committed
  ...</code></pre><p>This is Multi-Paxos. The Phase 1 cost is paid once per leader term, not once per log entry.</p>
<h2 id="leader-election">Leader Election</h2>
<p>Basic Paxos has no notion of a leader &ndash; any node can propose at any time, which is why it&rsquo;s correct but chaotic in practice. Multi-Paxos adds an informal leader election layer: one node is designated the distinguished proposer and the others voluntarily defer to it.</p>
<p>&ldquo;Voluntarily&rdquo; is the key word. If followers suspect the leader is dead (no heartbeats for a timeout period), any of them can run Phase 1 with a higher ballot to take over. The old leader doesn&rsquo;t get a veto &ndash; it just gets its Accept messages rejected because the acceptors have now promised a higher ballot.</p>
<p>This is where Multi-Paxos implementations diverge significantly. The Paxos paper doesn&rsquo;t specify how leader election works, what timeouts to use, or how to detect a live leader that&rsquo;s just partitioned. Every real implementation makes different choices here, which is part of why Multi-Paxos implementations are hard to compare.</p>
<h2 id="log-holes">Log Holes</h2>
<p>One complication basic Paxos avoids: with multiple slots running in parallel, you can get holes. The leader accepts slot 1 and slot 3 but slot 2 hasn&rsquo;t been filled yet because a client request was lost. Followers can&rsquo;t apply slot 3 to the state machine without knowing slot 2.</p>
<p>Multi-Paxos implementations handle this with a no-op: when a new leader wins an election, it runs Phase 1 for all uncommitted slots and fills any gaps with a no-op entry. This is what &ldquo;leader reconciliation&rdquo; looks like in practice &ndash; the new leader has to figure out what was committed before it took over and resolve any ambiguity.</p>
<p>Raft avoids holes entirely by requiring that the leader always send entries in order, with no gaps. This simplifies the implementation at the cost of some throughput (you can&rsquo;t pipeline out-of-order).</p>
<h2 id="zab-vs-multi-paxos">Zab vs. Multi-Paxos</h2>
<p>ZooKeeper uses Zab (ZooKeeper Atomic Broadcast), which is often described as &ldquo;Paxos-like&rdquo; but is actually closer to Multi-Paxos with some explicit differences.</p>
<p>The main one: Zab is designed around a primary-backup model where the primary sends updates in a strict FIFO order. The recovery protocol (Phase 1 equivalent in Zab) is designed to ensure the new primary has a superset of all committed entries before it starts serving. Zab also has an explicit synchronization phase where followers sync their logs to the new primary before it starts accepting writes.</p>
<p>Both tolerate the same number of failures and provide the same safety guarantees. The practical difference is that Zab is tuned for ZooKeeper&rsquo;s specific write pattern (broadcast to all followers, wait for majority ack, apply in order) while Multi-Paxos is a more general framework.</p>
<h2 id="multi-paxos-vs-raft">Multi-Paxos vs. Raft</h2>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Multi-Paxos</th>
          <th>Raft</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Leader election</td>
          <td>Ad hoc, not specified in paper</td>
          <td>Built into protocol with explicit terms</td>
      </tr>
      <tr>
          <td>Log holes</td>
          <td>Possible, requires no-op fill on leader change</td>
          <td>Not possible, entries sent in order</td>
      </tr>
      <tr>
          <td>Pipelining</td>
          <td>Supported natively</td>
          <td>Requires extensions</td>
      </tr>
      <tr>
          <td>Understandability</td>
          <td>Requires significant paper-reading and inference</td>
          <td>Designed to be implementable from the paper alone</td>
      </tr>
      <tr>
          <td>Real implementations</td>
          <td>Chubby (Google), custom variants</td>
          <td>etcd, CockroachDB, TiKV, Consul</td>
      </tr>
  </tbody>
</table>
<p>Multi-Paxos and Raft have the same fault tolerance and safety properties. The difference is that Raft specifies the things Multi-Paxos leaves up to the implementer &ndash; which is why Raft implementations tend to be more consistent across codebases.</p>
<p>Google&rsquo;s Chubby uses a Paxos variant internally. Google Spanner uses a Paxos variant per shard. In both cases Google built their own leader election and recovery logic on top of the basic protocol &ndash; which is exactly the work Raft was designed to eliminate.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Raft Log Compaction: Keeping the Log from Growing Forever</title>
      <link>https://KTS-o7.github.io/bear/systems/raft_log_compaction/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/raft_log_compaction/</guid>
      <description>&lt;p&gt;Raft&amp;rsquo;s correctness relies on the log. Every state change is appended to it, replicated to a majority, and then applied to the state machine. This works perfectly until you&amp;rsquo;ve been running for a few months and the log has millions of entries. Replaying the full log on every restart is slow. A new node joining the cluster would have to receive and apply every entry since the beginning of time.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Raft&rsquo;s correctness relies on the log. Every state change is appended to it, replicated to a majority, and then applied to the state machine. This works perfectly until you&rsquo;ve been running for a few months and the log has millions of entries. Replaying the full log on every restart is slow. A new node joining the cluster would have to receive and apply every entry since the beginning of time.</p>
<p>Log compaction is how you fix this. The idea is simple: once a prefix of the log has been applied to the state machine, you don&rsquo;t need the individual entries anymore &ndash; just a snapshot of the state at that point.</p>
<h2 id="snapshots">Snapshots</h2>
<p>A snapshot captures the complete state machine state at a given log index. Once the snapshot is written, all log entries up to and including that index can be discarded.</p>





<pre tabindex="0"><code>Before compaction:
  Log: [1: set x=1] [2: set y=2] [3: set x=3] [4: del y] [5: set z=1]
  Applied through index 4
  State machine: {x: 3, z: 1}

After compaction:
  Snapshot at index 4: {x: 3}  (z not in snapshot -- set z=1 not yet applied)
  Remaining log: [5: set z=1]</code></pre><p>Wait &ndash; index 5 is retained because it hasn&rsquo;t been applied yet when the snapshot is taken. The snapshot covers only committed, applied entries.</p>
<p>The snapshot metadata includes the last included index and last included term. These replace the log prefix. When AppendEntries includes a consistency check against the entry before the new one, the leader uses the snapshot&rsquo;s last index and term as the baseline when the follower&rsquo;s log has been compacted past that point.</p>
<h2 id="when-to-snapshot">When to Snapshot</h2>
<p>Most implementations trigger a snapshot when the log grows past a size threshold &ndash; say 64MB or 256MB. The Raft paper leaves this as an implementation detail. etcd uses 100MB by default. Smaller thresholds mean more frequent snapshots and less log replay on restart, but snapshot creation has a cost.</p>
<p>Taking a snapshot of a large state machine can be expensive. If you&rsquo;re snapshotting a database with gigabytes of data, the snapshot itself might take seconds. During that time the state machine is typically paused or copy-on-write semantics are used to take a consistent point-in-time snapshot without stopping serving. Most production implementations use copy-on-write (fork on Linux makes this cheap for process-based state machines).</p>
<h2 id="installsnapshot-rpc">InstallSnapshot RPC</h2>
<p>Snapshots matter most for a new node joining the cluster or a node that has been down for a long time and fallen significantly behind.</p>
<p>If a follower is so far behind that the leader has already compacted the log entries it needs, AppendEntries won&rsquo;t work &ndash; those entries are gone. Instead the leader sends the snapshot directly with InstallSnapshot RPC.</p>
<p>The follower receives the snapshot, writes it to stable storage, discards its entire existing log, and resets its state machine to the snapshot state. After that it can receive normal AppendEntries for the remaining log entries.</p>
<p>InstallSnapshot is also used when a brand new node joins the cluster. Rather than replaying potentially years of log history, the leader sends the current snapshot and the follower catches up from there.</p>
<h2 id="etcd-in-practice">etcd in Practice</h2>
<p>etcd runs a snapshot at 100MB by default (configurable with <code>--snapshot-count</code>, which actually counts committed entries, not bytes &ndash; the default is 10,000 entries). After snapshotting, etcd retains the last 5 snapshots and the WAL (write-ahead log) segments needed to replay from the oldest retained snapshot. This gives you a window of history for debugging without storing the full log.</p>
<p>etcd also exposes the snapshot mechanism for operator use. <code>etcdctl snapshot save</code> takes a snapshot of the current state to a file. This is the recommended backup mechanism for etcd clusters &ndash; you back up the snapshot, not the raw data directory.</p>
<h2 id="leader-leases-and-linearizable-reads">Leader Leases and Linearizable Reads</h2>
<p>One related problem: reads in a basic Raft cluster go through the leader, which needs to confirm it&rsquo;s still the leader before responding. The way to confirm is to send a heartbeat round to a majority before responding to a read &ndash; which is an extra round-trip per read, expensive under load.</p>
<p>Leader leases are the optimization. When a leader wins an election and sends its first heartbeats, it records the time and grants itself a lease for a fixed duration (typically the election timeout). During that window, it&rsquo;s guaranteed to still be the leader &ndash; any new leader would need a full election timeout to emerge. So it can serve reads locally without a heartbeat round.</p>
<p>The caveat: lease-based reads depend on the leader&rsquo;s clock being accurate relative to other nodes. If a leader&rsquo;s clock runs fast and its lease hasn&rsquo;t actually expired on other nodes&rsquo; clocks while it thinks it has, you can serve stale reads. etcd uses clock bounds and makes the lease duration conservative to avoid this. In practice, lease reads are the default in most etcd deployments because the latency benefit outweighs the theoretical risk on well-synchronized hardware clocks.</p>
<p>Raft&rsquo;s log compaction is not exciting to implement but it&rsquo;s essential for production deployments. A cluster without snapshot support will eventually fall over under its own log weight &ndash; or force every new node to replay history from the beginning, which is equally untenable.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Bulkhead Pattern: Isolating Failures Before They Spread</title>
      <link>https://KTS-o7.github.io/bear/systems/bulkhead_pattern/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/bulkhead_pattern/</guid>
      <description>&lt;p&gt;Circuit breakers stop you from hammering a failing downstream service. Bulkheads stop a failing downstream service from consuming all the resources you have &amp;ndash; even if you&amp;rsquo;re not hammering it.&lt;/p&gt;&#xA;&lt;p&gt;The name comes from ship design. A ship&amp;rsquo;s hull is divided into watertight compartments. One compartment floods, the others stay dry. The ship stays afloat. Without bulkheads, one breach sinks everything.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem-without-bulkheads&#34;&gt;The Problem Without Bulkheads&lt;/h2&gt;&#xA;&lt;p&gt;Imagine a service that calls three downstream APIs &amp;ndash; payment, inventory, and recommendations. You have a thread pool with 100 threads handling all outbound requests.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Circuit breakers stop you from hammering a failing downstream service. Bulkheads stop a failing downstream service from consuming all the resources you have &ndash; even if you&rsquo;re not hammering it.</p>
<p>The name comes from ship design. A ship&rsquo;s hull is divided into watertight compartments. One compartment floods, the others stay dry. The ship stays afloat. Without bulkheads, one breach sinks everything.</p>
<h2 id="the-problem-without-bulkheads">The Problem Without Bulkheads</h2>
<p>Imagine a service that calls three downstream APIs &ndash; payment, inventory, and recommendations. You have a thread pool with 100 threads handling all outbound requests.</p>
<p>The recommendations API starts responding slowly. Requests pile up waiting. Threads get tied up waiting for responses that take 10 seconds instead of 100ms. Within minutes all 100 threads are occupied waiting on recommendations. Now payment requests and inventory requests can&rsquo;t get a thread either. Your service is down for all three functions because one non-critical API got slow.</p>
<p>A circuit breaker would help here once the recommendations API is actually failing &ndash; but slow isn&rsquo;t failing. The requests are completing, just slowly. The circuit never trips. The thread pool still drains.</p>
<h2 id="the-fix-separate-resource-pools">The Fix: Separate Resource Pools</h2>
<p>Partition your resources so that one downstream dependency can only consume its own allocation.</p>
<p>Instead of one pool of 100 threads, you have three pools:</p>
<ul>
<li>Payment: 40 threads</li>
<li>Inventory: 40 threads</li>
<li>Recommendations: 20 threads</li>
</ul>
<p>The recommendations API goes slow. It consumes all 20 of its threads. Payment and inventory are unaffected &ndash; they have their own pools. The non-critical dependency is isolated in its own compartment.</p>
<h2 id="two-implementation-styles">Two Implementation Styles</h2>
<p><strong>Thread pool isolation</strong> is what Hystrix made popular. Each downstream call goes through a dedicated thread pool. Callers block on their own pool, not a shared one. Overhead: context switches between the caller thread and the pool thread for every call.</p>
<p><strong>Semaphore isolation</strong> is lighter. Instead of separate thread pools, you put a semaphore in front of each dependency with a max concurrent count. Callers execute on their own thread but must acquire the semaphore first. No thread switching overhead. The tradeoff: if the call actually blocks the thread (synchronous I/O), you&rsquo;re still tying up the caller&rsquo;s thread. Better for fast calls, worse for slow ones.</p>
<p>For services making lots of downstream calls with variable latency, thread pool isolation is generally safer. The overhead is real but the isolation is stronger.</p>
<h2 id="combining-with-circuit-breakers">Combining with Circuit Breakers</h2>
<p>Bulkheads and circuit breakers are complementary, not alternatives. They protect against different failure modes:</p>
<ul>
<li><strong>Bulkhead</strong>: limits resource consumption from a slow or partially degraded dependency</li>
<li><strong>Circuit breaker</strong>: stops calls entirely when a dependency crosses a failure threshold</li>
</ul>
<p>The typical pattern is both together. The bulkhead caps how many threads/connections one dependency can consume. The circuit breaker trips when that dependency starts returning errors. Between the two, you handle the full range of failure behavior from &ldquo;slightly slow&rdquo; to &ldquo;completely down.&rdquo;</p>
<p>Netflix Hystrix combined both in a single abstraction. Resilience4j does the same. In a service mesh like Istio, connection pool settings are the bulkhead and outlier detection is the circuit breaker &ndash; same ideas, configured at the mesh level rather than the application level.</p>
<h2 id="what-to-size-the-pools-at">What to Size the Pools At</h2>
<p>This is the hard part. Too small and you&rsquo;re rejecting valid traffic. Too large and the isolation breaks down.</p>
<p>A reasonable starting point: look at your normal concurrency for each dependency, add headroom for spikes, and set the max at roughly 2x your normal peak. Monitor pool utilization in steady state &ndash; if the recommendations pool is regularly at 18/20, you either need to size up or your recommendations calls are too slow.</p>
<p>Bulkheads don&rsquo;t fix slow dependencies. They contain the blast radius while you do.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Saga Outbox Pattern: Reliable Event Delivery Without Two-Phase Commit</title>
      <link>https://KTS-o7.github.io/bear/systems/saga_outbox_pattern/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/saga_outbox_pattern/</guid>
      <description>&lt;p&gt;The Saga pattern gives you distributed transactions without cross-service locks. Each step publishes an event when it completes, triggering the next step. But there&amp;rsquo;s a gap in the naive implementation: publishing an event and updating your local database are two separate operations. If your service crashes between them, you get a committed database write with no event published, or an event published with no matching database write. The saga is now in an inconsistent state.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The Saga pattern gives you distributed transactions without cross-service locks. Each step publishes an event when it completes, triggering the next step. But there&rsquo;s a gap in the naive implementation: publishing an event and updating your local database are two separate operations. If your service crashes between them, you get a committed database write with no event published, or an event published with no matching database write. The saga is now in an inconsistent state.</p>
<p>The outbox pattern closes that gap.</p>
<h2 id="the-dual-write-problem">The Dual-Write Problem</h2>
<p>Say the flight reservation step succeeds. You update your database to mark the reservation as confirmed and then publish a <code>FlightReserved</code> event to your message broker. These are two operations against two different systems. There&rsquo;s no way to make them atomic without 2PC &ndash; which is exactly what sagas are trying to avoid.</p>
<p>If you write to the database first and then crash before publishing, the event is never delivered. The hotel reservation step never starts. The saga stalls. Depending on your monitoring, this might sit undetected for a while.</p>
<p>If you publish first and then crash before writing to the database, you&rsquo;ve told the hotel step to proceed even though the flight reservation isn&rsquo;t durably recorded. The flight service comes back up with no record of the reservation. You&rsquo;ve introduced a phantom.</p>
<h2 id="the-outbox">The Outbox</h2>
<p>The fix: write the event to an outbox table in the same database, in the same local transaction as your state update. Then have a separate process relay events from the outbox to the message broker.</p>





<pre tabindex="0"><code>BEGIN TRANSACTION
  UPDATE flights SET status = &#39;reserved&#39; WHERE id = ?
  INSERT INTO outbox (event_type, payload, created_at)
    VALUES (&#39;FlightReserved&#39;, &#39;{&#34;booking_id&#34;: 123, ...}&#39;, NOW())
COMMIT</code></pre><p>The state change and the event are now atomic with respect to the database. Either both are committed or neither is. The outbox row is durable.</p>
<p>A separate relay process &ndash; sometimes called the outbox processor or message relay &ndash; polls the outbox table, publishes each event to the message broker, and marks the row as processed once delivery is confirmed.</p>





<pre tabindex="0"><code>Relay process:
  SELECT * FROM outbox WHERE processed_at IS NULL ORDER BY created_at LIMIT 100
  FOR EACH row:
    publish to message broker
    UPDATE outbox SET processed_at = NOW() WHERE id = row.id</code></pre><h2 id="at-least-once-delivery">At-Least-Once Delivery</h2>
<p>The relay delivers at-least-once. If the relay crashes after publishing but before marking the row as processed, it will publish the same event again on restart. Downstream consumers have to be idempotent &ndash; processing the same event twice should produce the same result as processing it once.</p>
<p>This is a real design constraint. Your hotel reservation step needs to check whether it already processed a given <code>FlightReserved</code> event before creating a new reservation. Deduplication is typically done with a unique constraint on the event ID or booking ID in the receiving service&rsquo;s database.</p>
<p>At-least-once with idempotent consumers is the standard contract in event-driven systems. Exactly-once is theoretically possible with additional coordination but practically rare &ndash; most systems accept at-least-once and invest in idempotency instead.</p>
<h2 id="change-data-capture-as-an-alternative-relay">Change Data Capture as an Alternative Relay</h2>
<p>Polling the outbox table works but adds load to the database. An alternative is Change Data Capture (CDC): instead of a polling process, you read directly from the database replication log.</p>
<p>Tools like Debezium connect to PostgreSQL&rsquo;s logical replication stream or MySQL&rsquo;s binlog and emit a change event for every row insert. Point Debezium at the outbox table and it publishes events automatically, with no polling query needed.</p>
<p>CDC-based relay has lower overhead and lower latency. The tradeoff: it&rsquo;s another piece of infrastructure to run and operate. For smaller deployments, polling is simpler. For high-throughput systems where polling lag or database load is a concern, CDC is worth it.</p>
<h2 id="saga-state-persistence">Saga State Persistence</h2>
<p>Related to the outbox is the question of where saga state itself lives.</p>
<p>The orchestrator driving the saga needs to know the current step, which steps have completed, and what the compensating actions are if something fails. Two common approaches:</p>
<p><strong>Dedicated saga store</strong>: the orchestrator writes saga state to its own database table after each step. Simple, queryable, easy to inspect when debugging a stuck saga. The outbox pattern applies here too &ndash; saga state update and event publish happen in the same transaction.</p>
<p><strong>Event sourcing</strong>: saga state is derived from the event log itself. There&rsquo;s no explicit state table &ndash; you reconstruct the saga&rsquo;s current position by replaying events. More complex to implement but gives you a full audit trail of everything that happened and an easy path to replaying failed sagas.</p>
<p>Most teams start with a dedicated saga store and move to event sourcing only if they need the audit trail or replay capability.</p>
<h2 id="what-fails-in-compensation">What Fails in Compensation</h2>
<p>One more gap worth naming: compensating transactions can fail too.</p>
<p>You cancel the hotel reservation as part of rolling back a failed booking. The cancel call times out. Now your saga is stuck in a partially compensated state &ndash; the flight is cancelled but the hotel reservation is still live.</p>
<p>The standard answer is retry with backoff. Compensating transactions need to be idempotent for the same reason forward steps do. The outbox pattern helps here too &ndash; compensation requests go through the same outbox mechanism, so they&rsquo;re durably enqueued even if the downstream service is temporarily unavailable.</p>
<p>If compensation keeps failing after retries, the saga is in a &ldquo;saga failure&rdquo; state that requires human intervention or a dead-letter queue. Building an operator UI for stuck sagas is not glamorous work, but it&rsquo;s necessary in any production saga implementation.</p>
<p>The outbox pattern doesn&rsquo;t make sagas simple. It removes the dual-write problem and gives you reliable event delivery &ndash; which turns a fundamentally broken approach into one that&rsquo;s merely complex.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Circuit Breakers: The Pattern That Stops Cascading Failures</title>
      <link>https://KTS-o7.github.io/bear/systems/circuit_breakers_distributed_systems/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/circuit_breakers_distributed_systems/</guid>
      <description>&lt;p&gt;The payment service starts timing out. The fraud detection API it depends on is slow. Your retry logic kicks in &amp;ndash; which makes sense, retries are good &amp;ndash; but now every incoming request is spawning 3x the load on a service that was already struggling. The fraud service falls over completely. Now the entire checkout flow is dead.&lt;/p&gt;&#xA;&lt;p&gt;This is a cascading failure. One slow dependency turned into a full outage because the upstream service kept hammering it instead of stopping.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The payment service starts timing out. The fraud detection API it depends on is slow. Your retry logic kicks in &ndash; which makes sense, retries are good &ndash; but now every incoming request is spawning 3x the load on a service that was already struggling. The fraud service falls over completely. Now the entire checkout flow is dead.</p>
<p>This is a cascading failure. One slow dependency turned into a full outage because the upstream service kept hammering it instead of stopping.</p>
<h2 id="the-pattern">The Pattern</h2>
<p>Circuit breakers fix this by failing fast. The name comes from electrical circuit breakers: when the current exceeds a threshold, the breaker trips and cuts power before anything burns. Same idea here.</p>
<p>The circuit has three states:</p>
<p><strong>Closed</strong> &ndash; normal operation. Requests go through, failures are counted. When failures cross a configured threshold within a time window, the circuit trips.</p>
<p><strong>Open</strong> &ndash; the circuit has tripped. Requests immediately return an error without calling the downstream service at all. No network round-trips, no timeouts, no extra load on a service that&rsquo;s already struggling. After a cooldown period, the circuit moves to half-open.</p>
<p><strong>Half-open</strong> &ndash; one test request is allowed through. If it succeeds, the circuit closes and normal operation resumes. If it fails, the circuit trips back to open and the cooldown resets.</p>
<p>The key insight is that sometimes the most useful thing you can do is stop trying. Retrying a failing service doesn&rsquo;t help it recover. Backing off does.</p>
<h2 id="where-youll-find-it">Where You&rsquo;ll Find It</h2>
<p>Netflix Hystrix made this pattern famous, but it&rsquo;s now table stakes in most infrastructure:</p>
<ul>
<li>Istio has it built into the service mesh at the sidecar level &ndash; no application code needed</li>
<li>Resilience4j for Java</li>
<li>Polly for .NET</li>
<li>Most Redis clients ship with circuit breaking on connection failures</li>
</ul>
<p>The mesh-level implementations are interesting because they work across languages and don&rsquo;t require every service team to implement the pattern themselves. You configure thresholds in the mesh and it handles the state machine.</p>
<h2 id="the-gotcha-timeout-ordering">The Gotcha: Timeout Ordering</h2>
<p>There&rsquo;s one configuration mistake that makes circuit breakers useless: setting your request timeout longer than your circuit breaker&rsquo;s failure window.</p>
<p>If your timeout is 30 seconds but your circuit breaker trips after 5 failures within 10 seconds &ndash; and each failure takes 25 seconds to time out &ndash; you&rsquo;ll accumulate only one or two failures in any 10-second window. The circuit never trips because failures arrive too slowly to hit the threshold, even while the system is drowning in slow requests.</p>
<p>The rule: timeouts must be shorter than the failure window. If your breaker watches a 10-second window and trips at 5 failures, your per-request timeout needs to be well under 2 seconds. Otherwise the breaker is just decorative.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Consistent Hashing: Scaling a Cluster Without Reshuffling the World</title>
      <link>https://KTS-o7.github.io/bear/systems/consistent_hashing/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/consistent_hashing/</guid>
      <description>&lt;p&gt;The naive approach to distributing keys across N cache nodes is &lt;code&gt;hash(key) % N&lt;/code&gt;. It works fine until you add or remove a node.&lt;/p&gt;&#xA;&lt;p&gt;When N changes to N+1 or N-1, nearly every key remaps to a different node. You go from a warm cache to a cold one in an instant &amp;ndash; a thundering herd of cache misses hitting your database all at once. For a large cluster, that&amp;rsquo;s enough to take down a service.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The naive approach to distributing keys across N cache nodes is <code>hash(key) % N</code>. It works fine until you add or remove a node.</p>
<p>When N changes to N+1 or N-1, nearly every key remaps to a different node. You go from a warm cache to a cold one in an instant &ndash; a thundering herd of cache misses hitting your database all at once. For a large cluster, that&rsquo;s enough to take down a service.</p>
<p>Consistent hashing fixes this. Add or remove a node, and only the keys that were assigned to that node need to move. Everything else stays put.</p>
<h2 id="the-hash-ring">The Hash Ring</h2>
<p>Instead of <code>hash(key) % N</code>, arrange all possible hash values on a ring from 0 to 2^32. Each node gets a position on the ring by hashing its name or ID. To find which node owns a key: hash the key, then walk clockwise around the ring until you hit a node.</p>





<pre tabindex="0"><code>          0
         / \
    Node C   Node A
       |         |
    Node B --- Node D</code></pre><p>Key K hashes to a point on the ring. The first node clockwise from that point owns K.</p>
<p>Now add a new node between B and C. Only the keys that were between B and the new node&rsquo;s position need to move &ndash; from C to the new node. Everything else is unaffected. In an N-node cluster, adding one node moves roughly 1/N of the keys instead of nearly all of them.</p>
<h2 id="virtual-nodes">Virtual Nodes</h2>
<p>Raw consistent hashing has a load distribution problem. With a small number of physical nodes, the gaps between them on the ring are uneven &ndash; one node might own 30% of the keyspace, another 10%. Remove a node and all its load lands on a single neighbor.</p>
<p>Virtual nodes fix this: each physical node gets multiple positions on the ring, typically by hashing it with different suffixes (<code>node1-0</code>, <code>node1-1</code>, <code>node1-2</code>, &hellip;). With enough virtual nodes per physical node, the load distributes evenly across the ring. Removing one node scatters its keys across many neighbors rather than dumping them all on one.</p>
<p>This is the production configuration in every serious deployment.</p>
<h2 id="where-its-used">Where It&rsquo;s Used</h2>
<ul>
<li><strong>Amazon DynamoDB</strong> &ndash; consistent hashing across storage nodes, virtual nodes for even distribution</li>
<li><strong>Apache Cassandra</strong> &ndash; the token ring is consistent hashing; each node owns a range of tokens</li>
<li><strong>Memcached</strong> &ndash; the ketama client implements consistent hashing for cache node assignment</li>
<li>Basically every distributed cache or storage system that needs to scale nodes without full reshuffling</li>
</ul>
<p>The pattern generalizes beyond caching. Any system that needs to assign work or data to nodes &ndash; and occasionally add or remove nodes &ndash; runs into the same problem. Consistent hashing is the standard solution.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Paxos: How Distributed Systems Agree on Anything</title>
      <link>https://KTS-o7.github.io/bear/systems/paxos_consensus/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/paxos_consensus/</guid>
      <description>&lt;p&gt;Every replicated database, every distributed lock, every leader election comes down to the same underlying problem: how do multiple nodes in an unreliable network agree on a single value, even when some of them crash or messages get delayed?&lt;/p&gt;&#xA;&lt;p&gt;This is the consensus problem. Paxos is the foundational answer. It&amp;rsquo;s also notoriously difficult to implement correctly &amp;ndash; which is exactly why Raft exists. But you can&amp;rsquo;t really understand Raft without understanding what Paxos is doing and why.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Every replicated database, every distributed lock, every leader election comes down to the same underlying problem: how do multiple nodes in an unreliable network agree on a single value, even when some of them crash or messages get delayed?</p>
<p>This is the consensus problem. Paxos is the foundational answer. It&rsquo;s also notoriously difficult to implement correctly &ndash; which is exactly why Raft exists. But you can&rsquo;t really understand Raft without understanding what Paxos is doing and why.</p>
<h2 id="the-constraint">The Constraint</h2>
<p>Two-phase commit solves distributed atomicity but requires the coordinator to stay up. If the coordinator crashes after phase 1, participants are stuck holding locks, unable to proceed. That&rsquo;s not consensus &ndash; that&rsquo;s a single point of failure with extra steps.</p>
<p>Paxos removes that constraint. It guarantees agreement as long as a majority of nodes are up, regardless of which specific nodes fail. Any node can propose a value. No permanent coordinator required.</p>
<h2 id="roles">Roles</h2>
<p>In Paxos, nodes play three roles &ndash; and in practice, any node can play any of them:</p>
<ul>
<li><strong>Proposer</strong> &ndash; initiates a round, tries to get a value accepted</li>
<li><strong>Acceptor</strong> &ndash; votes on proposals, must follow the protocol strictly</li>
<li><strong>Learner</strong> &ndash; learns the final chosen value once consensus is reached</li>
</ul>
<h2 id="the-protocol">The Protocol</h2>
<p>Paxos runs in two phases.</p>
<h3 id="phase-1--prepare--promise">Phase 1 &ndash; Prepare / Promise</h3>
<p>The proposer picks a proposal number <code>n</code> &ndash; unique, higher than any it has seen before &ndash; and sends <code>PREPARE(n)</code> to a quorum of acceptors (any majority).</p>
<p>Each acceptor that receives this:</p>
<ul>
<li>Promises to ignore any future <code>PREPARE</code> with a lower number</li>
<li>If it has already accepted a value in a prior round, includes that <code>(old_n, old_value)</code> in its reply</li>
</ul>
<h3 id="phase-2--accept--accepted">Phase 2 &ndash; Accept / Accepted</h3>
<p>If the proposer receives promises from a quorum:</p>
<ul>
<li>If any promise included a prior accepted value, the proposer <strong>must use the highest one</strong> &ndash; it cannot choose freely</li>
<li>Otherwise it can propose any value it wants</li>
</ul>
<p>It sends <code>ACCEPT(n, value)</code> to the quorum. Acceptors that haven&rsquo;t made a conflicting promise write the value and reply <code>ACCEPTED</code>. Once a quorum accepts, the value is chosen.</p>





<pre tabindex="0"><code>Proposer       Acceptor A    Acceptor B    Acceptor C
   |-- PREPARE(5) --&gt;|             |             |
   |-- PREPARE(5) ---|------------&gt;|             |
   |&lt;-- PROMISE(5) --|             |             |
   |&lt;-- PROMISE(5) --|-------------|             |
   |-- ACCEPT(5,v) -&gt;|             |             |
   |-- ACCEPT(5,v) --|------------&gt;|             |
   |&lt;-- ACCEPTED ----|             |             |
   |&lt;-- ACCEPTED ----|-------------|             |
      [value v is now chosen]</code></pre><h2 id="why-its-correct">Why It&rsquo;s Correct</h2>
<p>The safety guarantee &ndash; that no two values are ever chosen &ndash; comes from the quorum overlap. Any two majorities in an N-node cluster share at least one node. That shared node is the bridge: if a value was accepted in round <code>n</code>, a new proposer in round <code>n+1</code> will hear about it from the shared node and be forced to continue it rather than propose something different.</p>
<p>The constraint that proposers must adopt the highest prior accepted value is what makes this work. A new round can never silently override an old one.</p>
<h2 id="the-gotchas">The Gotchas</h2>
<p><strong>Livelock.</strong> Two competing proposers can keep preempting each other with higher proposal numbers indefinitely &ndash; neither ever reaching phase 2. The fix is to elect a single distinguished proposer (a leader) and have everyone else defer to it. This is what Multi-Paxos does.</p>
<p><strong>Single-value vs. Multi-Paxos.</strong> Basic Paxos agrees on one value. Real systems need a log of values &ndash; a sequence of commands. Multi-Paxos runs Paxos repeatedly for each log slot with a stable leader, amortizing the phase 1 cost across many entries. Once a leader is established, phase 1 only needs to run once per leader lifetime.</p>
<p><strong>Implementation gap.</strong> The original paper was intentionally abstract. &ldquo;Paxos Made Simple&rdquo; is cleaner but still leaves enormous implementation latitude &ndash; enough that teams have shipped subtly broken versions. The gap between the protocol description and a correct, production-safe implementation is one of the main reasons Raft was designed as an explicit replacement.</p>
<h2 id="where-it-runs">Where It Runs</h2>
<ul>
<li><strong>Google Chubby</strong> &ndash; distributed lock service, underpins Bigtable and GFS coordination</li>
<li><strong>Apache ZooKeeper</strong> &ndash; uses Zab (ZooKeeper Atomic Broadcast), a Paxos variant</li>
<li><strong>Google Spanner</strong> &ndash; Paxos per shard for replication across datacenters</li>
</ul>
<p>Paxos is the theory. If you&rsquo;re building something new, you&rsquo;ll probably implement Raft instead &ndash; same guarantees, actually debuggable. But the mental model of quorum-based two-phase consensus is worth having regardless of which one you reach for.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Raft: Consensus for the Rest of Us</title>
      <link>https://KTS-o7.github.io/bear/systems/raft_consensus/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/raft_consensus/</guid>
      <description>&lt;p&gt;Paxos is the algorithm everyone cites and almost nobody implements correctly. The gap between the paper and a working, production-safe implementation is wide enough that teams have shipped subtly broken versions without knowing it for months.&lt;/p&gt;&#xA;&lt;p&gt;Raft was built as a direct response to that. Same safety guarantees, designed from scratch for understandability &amp;ndash; the explicit goal was that a student should be able to implement it correctly from the paper alone.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Paxos is the algorithm everyone cites and almost nobody implements correctly. The gap between the paper and a working, production-safe implementation is wide enough that teams have shipped subtly broken versions without knowing it for months.</p>
<p>Raft was built as a direct response to that. Same safety guarantees, designed from scratch for understandability &ndash; the explicit goal was that a student should be able to implement it correctly from the paper alone.</p>
<p>It works. etcd runs on it. Kubernetes&rsquo; entire cluster state lives in etcd. CockroachDB uses a Raft group per range. TiKV, Consul &ndash; Raft is everywhere Paxos was supposed to be.</p>
<h2 id="the-decomposition">The Decomposition</h2>
<p>Raft&rsquo;s core move is breaking consensus into three subproblems that can be understood and implemented independently:</p>
<ol>
<li>Leader election &ndash; pick one node as leader at any time</li>
<li>Log replication &ndash; leader accepts entries, replicates to followers</li>
<li>Safety &ndash; no two nodes ever commit conflicting entries</li>
</ol>
<p>Everything flows through a single leader. There&rsquo;s no &ldquo;any node can propose&rdquo; ambiguity like in basic Paxos. The leader is the sole source of truth for what enters the log. That constraint makes the rest of the protocol tractable.</p>
<h2 id="terms-and-roles">Terms and Roles</h2>
<p>Raft uses a logical clock called a <strong>term</strong> &ndash; a monotonically increasing integer. Each election starts a new term. If a node sees a message from a higher term than its own, it immediately updates and steps down if it was leader. Terms are how stale leaders get detected and evicted.</p>
<p>Every node is one of three things at any moment:</p>
<ul>
<li><strong>Leader</strong> &ndash; accepts client requests, replicates entries, sends heartbeats to prevent new elections</li>
<li><strong>Follower</strong> &ndash; passive, responds to RPCs from leaders and candidates</li>
<li><strong>Candidate</strong> &ndash; transitional state during an election</li>
</ul>
<p>Every node starts as a follower.</p>
<h2 id="leader-election">Leader Election</h2>
<p>Followers expect periodic heartbeats from the leader via AppendEntries RPCs (sent even when there are no entries to replicate). If an election timeout fires without a heartbeat, the follower converts to a candidate:</p>
<ol>
<li>Increment current term, vote for self</li>
<li>Send RequestVote to all peers</li>
<li>A peer grants its vote if it hasn&rsquo;t voted this term and the candidate&rsquo;s log is at least as up-to-date as its own</li>
<li>First candidate to collect a majority wins, becomes leader, starts sending heartbeats immediately</li>
</ol>
<p>Split votes happen when two candidates start elections simultaneously and each gets half the cluster. Neither reaches majority. Both time out and retry with a new term. Raft handles this with randomized election timeouts (typically 150&ndash;300ms) &ndash; making simultaneous timeouts unlikely without coordination overhead.</p>
<h2 id="log-replication">Log Replication</h2>
<p>Once a leader is elected:</p>
<ol>
<li>Client sends a command to the leader</li>
<li>Leader appends the entry to its local log (not yet committed)</li>
<li>Leader sends AppendEntries to all followers in parallel</li>
<li>Once a majority have written the entry, the leader marks it committed</li>
<li>Leader applies the entry to its state machine, replies to client</li>
<li>On the next heartbeat, followers learn the commit index and apply the entry too</li>
</ol>





<pre tabindex="0"><code>Client → [write x=5] → Leader
Leader  → AppendEntries(x=5) → Follower A  ✓
Leader  → AppendEntries(x=5) → Follower B  ✓
Leader  → AppendEntries(x=5) → Follower C  (unreachable)
[Majority = Leader + A + B → COMMIT] → apply x=5</code></pre><p>Follower C is behind but not a problem. When it reconnects, the leader sends it the missing entries.</p>
<h2 id="safety-log-matching">Safety: Log Matching</h2>
<p>Raft&rsquo;s key safety property: if two logs contain an entry with the same index and term, all preceding entries are identical.</p>
<p>This is enforced by the AppendEntries consistency check. When the leader sends an entry, it includes the index and term of the entry immediately before it. A follower rejects the append if that preceding entry doesn&rsquo;t match its own log. The leader then backs up and resends from an earlier point until it finds a common prefix, then overwrites the diverged entries.</p>
<p>The vote restriction reinforces this: a node won&rsquo;t vote for a candidate whose log is less up-to-date than its own. This guarantees the new leader always has all committed entries &ndash; it never needs to fetch missing entries from other nodes after winning an election.</p>
<h2 id="leader-failure">Leader Failure</h2>
<p>If the leader crashes, the election timeout fires on one of the followers and a new election starts. The vote restriction ensures the winner has the most complete log among the majority. No repair phase, no querying all nodes &ndash; safety comes from the constraint, not from post-election cleanup.</p>
<h2 id="raft-vs-paxos">Raft vs. Paxos</h2>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Multi-Paxos</th>
          <th>Raft</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Design goal</td>
          <td>Correctness</td>
          <td>Correctness + understandability</td>
      </tr>
      <tr>
          <td>Leader</td>
          <td>Distinguished proposer (informal)</td>
          <td>Explicit, enforced by protocol</td>
      </tr>
      <tr>
          <td>Log holes</td>
          <td>Can have gaps, complex to fill</td>
          <td>No gaps, entries apply in order</td>
      </tr>
      <tr>
          <td>Config changes</td>
          <td>Ad hoc</td>
          <td>Joint consensus built in</td>
      </tr>
      <tr>
          <td>Implementations</td>
          <td>Chubby, Zab (variants)</td>
          <td>etcd, CockroachDB, TiKV, Consul</td>
      </tr>
  </tbody>
</table>
<p>Both tolerate up to (N-1)/2 failures in an N-node cluster. The fault tolerance is identical. The difference is that Raft implementations are actually correct in practice because the protocol is debuggable.</p>
<h2 id="where-it-runs">Where It Runs</h2>
<ul>
<li><strong>etcd</strong> &ndash; Kubernetes&rsquo; backing store; the entire cluster state (pods, services, configs) is a Raft log</li>
<li><strong>CockroachDB</strong> &ndash; Raft per range; each 512MB range is an independent 3&ndash;5 node Raft group</li>
<li><strong>TiKV</strong> &ndash; Raft groups per region, underpins TiDB</li>
<li><strong>Consul</strong> &ndash; service discovery and distributed KV</li>
</ul>
<p>If you&rsquo;re building anything that needs replicated state and strong consistency, Raft is the default answer. The paper is readable in an afternoon. The TLA+ spec exists if you want to verify your implementation. Start there.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Saga Pattern: Distributed Transactions Without the Lock</title>
      <link>https://KTS-o7.github.io/bear/systems/saga_pattern/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/saga_pattern/</guid>
      <description>&lt;p&gt;Two-phase commit gives you distributed atomicity. It also gives you a coordinator that can stall your entire system if it crashes at the wrong moment, and locks held across multiple services for however long the transaction takes. For short, internal transactions that&amp;rsquo;s manageable. For a trip booking flow that touches a flight service, a hotel service, and a payment service &amp;ndash; each owned by a different team, each potentially slow &amp;ndash; it&amp;rsquo;s not.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Two-phase commit gives you distributed atomicity. It also gives you a coordinator that can stall your entire system if it crashes at the wrong moment, and locks held across multiple services for however long the transaction takes. For short, internal transactions that&rsquo;s manageable. For a trip booking flow that touches a flight service, a hotel service, and a payment service &ndash; each owned by a different team, each potentially slow &ndash; it&rsquo;s not.</p>
<p>The Saga pattern is the alternative. You give up true atomicity in exchange for availability, scalability, and no cross-service locks.</p>
<h2 id="how-it-works">How It Works</h2>
<p>Break one big distributed transaction into a sequence of local transactions, each owned by a single service. Each step publishes an event or sends a message when it completes. If any step fails, run compensating transactions in reverse to undo the completed steps.</p>
<p>The classic example is a trip booking:</p>





<pre tabindex="0"><code>Step 1: Reserve flight    → local tx in Flight Service
Step 2: Reserve hotel     → local tx in Hotel Service
Step 3: Charge card       → local tx in Payment Service</code></pre><p>If step 3 fails:</p>





<pre tabindex="0"><code>FORWARD:   [Flight ✓] → [Hotel ✓] → [Payment ✗]
ROLLBACK:              ← [Cancel Hotel] ← [Cancel Flight]</code></pre><p>No global rollback. No coordinator holding locks across all three services. Each service knows how to undo its own step.</p>
<h2 id="two-flavors">Two Flavors</h2>
<p><strong>Choreography</strong> &ndash; each service listens for events and decides what to do next based on what it hears. No central coordinator. Fully decoupled. Easy to start, and a debugging nightmare once you have more than a handful of services. Tracing a failed saga means reconstructing a sequence of events scattered across multiple event streams.</p>
<p><strong>Orchestration</strong> &ndash; a central saga orchestrator drives the sequence. It tells each service what to do, collects results, and issues compensating calls on failure. Full visibility into saga state. Easier to reason about and monitor. The orchestrator becomes a coordination hub &ndash; not quite a single point of failure if you make it durable, but a complexity center. Most teams end up here in production.</p>
<h2 id="the-tradeoff-you-cant-avoid">The Tradeoff You Can&rsquo;t Avoid</h2>
<p>Sagas trade the &ldquo;I&rdquo; in ACID (isolation) for availability. While a saga is in-flight, intermediate state is visible to other parts of the system. A hotel room might be reserved even though the flight hasn&rsquo;t been confirmed yet. Other services can observe that state.</p>
<p>You have to design for this explicitly:</p>
<ul>
<li><strong>Eventual consistency</strong> &ndash; the system converges to a correct final state, but not atomically</li>
<li><strong>Idempotent compensating transactions</strong> &ndash; if your &ldquo;cancel flight&rdquo; call gets retried twice due to a network hiccup, it must be safe to run twice. Compensation that isn&rsquo;t idempotent will make your failure recovery worse than the original failure.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Property</th>
          <th>2PC</th>
          <th>Saga</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Atomicity</td>
          <td>Global</td>
          <td>Per-step only</td>
      </tr>
      <tr>
          <td>Isolation</td>
          <td>Full</td>
          <td>Intermediate state visible</td>
      </tr>
      <tr>
          <td>Availability</td>
          <td>Blocks on coordinator failure</td>
          <td>Always makes progress</td>
      </tr>
      <tr>
          <td>Scalability</td>
          <td>Locks across services</td>
          <td>No cross-service locks</td>
      </tr>
  </tbody>
</table>
<h2 id="where-it-shows-up">Where It Shows Up</h2>
<p>Basically every large-scale order or booking flow:</p>
<ul>
<li><strong>Uber</strong> &ndash; ride booking, driver assignment, payment charge, all as separate service transactions</li>
<li><strong>Amazon</strong> &ndash; order placement, inventory reservation, fulfillment, shipping</li>
<li><strong>Netflix</strong> &ndash; subscription management, content licensing flows</li>
</ul>
<p>These systems can&rsquo;t afford to hold cross-service locks for the duration of a user-facing transaction. They accept that intermediate states exist and design their compensating logic carefully.</p>
<p>The saga pattern doesn&rsquo;t make distributed transactions easy. It makes the tradeoffs explicit and gives you a way to build systems that stay available even when individual steps fail.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Two-Phase Commit: Distributed Atomicity and Its Fatal Flaw</title>
      <link>https://KTS-o7.github.io/bear/systems/two_phase_commit/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/two_phase_commit/</guid>
      <description>&lt;p&gt;The problem is simple to state: you have a transaction that touches multiple nodes. You want all of them to commit, or none of them to. No partial states, no one node committing while another aborts.&lt;/p&gt;&#xA;&lt;p&gt;Two-phase commit (2PC) solves this. It&amp;rsquo;s been in production databases for decades. It also has a flaw that makes it unusable for anything long-running, which is why most modern large-scale systems don&amp;rsquo;t use it directly.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The problem is simple to state: you have a transaction that touches multiple nodes. You want all of them to commit, or none of them to. No partial states, no one node committing while another aborts.</p>
<p>Two-phase commit (2PC) solves this. It&rsquo;s been in production databases for decades. It also has a flaw that makes it unusable for anything long-running, which is why most modern large-scale systems don&rsquo;t use it directly.</p>
<h2 id="the-protocol">The Protocol</h2>
<p>There&rsquo;s a coordinator and a set of participants. The coordinator drives the protocol.</p>
<p><strong>Phase 1 &ndash; Prepare</strong></p>
<p>The coordinator sends <code>PREPARE</code> to every participant. Each participant:</p>
<ul>
<li>Writes the transaction to a durable log</li>
<li>Acquires the necessary locks</li>
<li>Replies <code>YES</code> (ready to commit) or <code>NO</code> (something failed)</li>
</ul>
<p><strong>Phase 2 &ndash; Commit or Abort</strong></p>
<ul>
<li>If all participants said YES: coordinator sends <code>COMMIT</code> to everyone</li>
<li>If anyone said NO: coordinator sends <code>ABORT</code></li>
</ul>
<p>Participants execute the decision and release their locks.</p>





<pre tabindex="0"><code>Coordinator         Node A        Node B
    |--- PREPARE -------&gt;|             |
    |--- PREPARE ---------|-----------&gt;|
    |&lt;-- YES ------------|             |
    |&lt;-- YES ------------|-------------|
    |--- COMMIT --------&gt;|             |
    |--- COMMIT ----------|-----------&gt;|</code></pre><p>Clean path works fine. Every node ends up in the same state. Locks are held only for the duration of the protocol.</p>
<h2 id="the-fatal-flaw">The Fatal Flaw</h2>
<p>Crash the coordinator after phase 1 but before phase 2.</p>
<p>Participants are now stuck. They voted YES and locked their resources. They don&rsquo;t know whether the coordinator decided to commit or abort. They can&rsquo;t safely proceed in either direction &ndash; committing might be wrong if another participant said NO, aborting might be wrong if the coordinator was about to send COMMIT.</p>
<p>They wait. The locks stay held. The system is in limbo until the coordinator recovers.</p>
<p>This is the <strong>blocking problem</strong>. 2PC is a blocking protocol under failure. A single coordinator crash can stall the entire transaction indefinitely.</p>
<h2 id="the-attempted-fix-3pc">The Attempted Fix: 3PC</h2>
<p>Three-phase commit inserts a pre-commit phase between prepare and commit:</p>
<p><code>PREPARE → PRE-COMMIT → COMMIT</code></p>
<p>Pre-commit lets participants infer the coordinator&rsquo;s intent even if it crashes: if you received a pre-commit, the coordinator had seen all YES votes and was going to commit. You can safely commit yourself if the coordinator disappears after that point.</p>
<p>3PC eliminates most blocking scenarios. It costs one extra round of messages and still has edge cases under network partitions. It&rsquo;s not widely used in practice &ndash; the complexity tradeoff isn&rsquo;t worth it for most systems.</p>
<h2 id="what-systems-actually-do">What Systems Actually Do</h2>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>Mechanism</th>
          <th>Tradeoff</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Saga pattern</td>
          <td>Compensating transactions</td>
          <td>No locks, but no true atomicity</td>
      </tr>
      <tr>
          <td>Paxos / Raft</td>
          <td>Consensus algorithm</td>
          <td>Strong guarantees, higher complexity</td>
      </tr>
      <tr>
          <td>Optimistic concurrency</td>
          <td>Detect conflicts at commit time</td>
          <td>Works well for low-contention workloads</td>
      </tr>
  </tbody>
</table>
<p>Most large-scale systems avoid 2PC for anything user-facing. Short, internal database transactions &ndash; a single service committing to one database &ndash; are fine. Cross-service transactions that could run for seconds or involve user interaction are not.</p>
<p>2PC is worth understanding because it makes the tradeoff concrete: you can have distributed atomicity, but you&rsquo;re paying for it with availability under coordinator failure. Everything else in distributed transactions is a different point on that curve.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Vector Clocks: Causality Without a Shared Clock</title>
      <link>https://KTS-o7.github.io/bear/systems/vector_clocks/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/vector_clocks/</guid>
      <description>&lt;p&gt;In a distributed system there&amp;rsquo;s no shared clock. Two nodes can&amp;rsquo;t agree on what time it is &amp;ndash; network latency is variable, clocks drift, and NTP only gets you so far. So you can&amp;rsquo;t use timestamps to order events across nodes in any meaningful way.&lt;/p&gt;&#xA;&lt;p&gt;The question is: can you still tell whether one event caused another?&lt;/p&gt;&#xA;&lt;p&gt;Vector clocks answer that question. They don&amp;rsquo;t give you wall-clock time. They give you something more useful: a precise definition of &amp;ldquo;happened before.&amp;rdquo;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In a distributed system there&rsquo;s no shared clock. Two nodes can&rsquo;t agree on what time it is &ndash; network latency is variable, clocks drift, and NTP only gets you so far. So you can&rsquo;t use timestamps to order events across nodes in any meaningful way.</p>
<p>The question is: can you still tell whether one event caused another?</p>
<p>Vector clocks answer that question. They don&rsquo;t give you wall-clock time. They give you something more useful: a precise definition of &ldquo;happened before.&rdquo;</p>
<h2 id="how-they-work">How They Work</h2>
<p>Each node maintains a vector of counters, one slot per node in the system. Call them <code>[A, B, C]</code> for a three-node cluster.</p>
<p>Three rules:</p>
<ol>
<li><strong>Local event</strong> &ndash; increment your own counter</li>
<li><strong>Send a message</strong> &ndash; attach the current vector to the message</li>
<li><strong>Receive a message</strong> &ndash; merge by taking the element-wise max of your vector and the received vector, then increment your own counter</li>
</ol>
<p>A concrete trace:</p>





<pre tabindex="0"><code>Node A starts:  [A:0, B:0, C:0]
Node A event:   [A:1, B:0, C:0]
Node A sends to B, attaches [A:1, B:0, C:0]

Node B receives, merges: max([A:0,B:0,C:0], [A:1,B:0,C:0]) = [A:1,B:0,C:0]
Node B increments own:   [A:1, B:1, C:0]
Node B event:            [A:1, B:2, C:0]</code></pre><h2 id="what-you-can-determine">What You Can Determine</h2>
<p>Given two events with vectors <code>V1</code> and <code>V2</code>, exactly one of three things is true:</p>
<table>
  <thead>
      <tr>
          <th>Result</th>
          <th>Condition</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>V1 happened-before V2</td>
          <td>V1[i] &lt;= V2[i] for every i, and strict for at least one</td>
      </tr>
      <tr>
          <td>V2 happened-before V1</td>
          <td>V2[i] &lt;= V1[i] for every i, and strict for at least one</td>
      </tr>
      <tr>
          <td><strong>Concurrent</strong></td>
          <td>Neither vector dominates</td>
      </tr>
  </tbody>
</table>
<p>The concurrent case is the important one. When neither vector dominates, the events genuinely have no causal relationship &ndash; they happened independently on different nodes without either one knowing about the other. No ordering can be inferred because no ordering exists.</p>
<h2 id="why-this-matters">Why This Matters</h2>
<p>The alternative is last-write-wins: when two writes conflict, pick the one with the later timestamp and discard the other. This is simple to implement and silently destroys data. If two users edit the same document concurrently, you lose one of their changes without telling anyone.</p>
<p>Vector clocks let you detect that conflict honestly. DynamoDB and Riak use them (or variants) to surface conflicting writes to the application rather than silently resolving them. The application &ndash; or the user &ndash; can then merge the two versions intelligently.</p>
<p>&ldquo;Happened-before&rdquo; is a relationship you earn through the causal structure of the system, not one you assume from timestamps.</p>
<h2 id="the-tradeoff">The Tradeoff</h2>
<p>Vector size grows O(N) with the number of nodes. In a cluster with hundreds of nodes, attaching a full vector to every message gets expensive.</p>
<p><strong>Dotted version vectors</strong> are a modern fix: they compress the vector by pruning entries that are causally dominated. Same core semantics, better space efficiency for large clusters.</p>
<p>Lamport clocks are the scalar predecessor &ndash; a single counter instead of a vector. They give you a consistent ordering (if A happened-before B, A&rsquo;s timestamp is lower), but they don&rsquo;t detect concurrency. Two events with different Lamport timestamps might still be concurrent. Vector clocks are strictly more informative.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Paxos</title>
      <link>https://KTS-o7.github.io/bear/systems/paxos/</link>
      <pubDate>Sun, 26 Apr 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/paxos/</guid>
      <description>&lt;p&gt;Before Raft, there was Paxos. For about two decades it was the foundational answer to distributed consensus &amp;ndash; and it was notoriously hard to implement correctly. Lamport published the original paper in 1989, but it was so difficult to parse that he rewrote it as &amp;ldquo;Paxos Made Simple&amp;rdquo; in 2001. The simpler version is still genuinely hard.&lt;/p&gt;&#xA;&lt;p&gt;Understanding Paxos is worth doing. It&amp;rsquo;s not just historical &amp;ndash; Chubby, ZooKeeper, and Spanner are built on it or close variants, and many distributed systems interviews probe it. More importantly, every simplification Raft makes is a direct response to a specific pain point in Paxos. Understanding what Paxos gets wrong explains why Raft looks the way it does.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Before Raft, there was Paxos. For about two decades it was the foundational answer to distributed consensus &ndash; and it was notoriously hard to implement correctly. Lamport published the original paper in 1989, but it was so difficult to parse that he rewrote it as &ldquo;Paxos Made Simple&rdquo; in 2001. The simpler version is still genuinely hard.</p>
<p>Understanding Paxos is worth doing. It&rsquo;s not just historical &ndash; Chubby, ZooKeeper, and Spanner are built on it or close variants, and many distributed systems interviews probe it. More importantly, every simplification Raft makes is a direct response to a specific pain point in Paxos. Understanding what Paxos gets wrong explains why Raft looks the way it does.</p>
<h2 id="the-problem">The Problem</h2>
<p>Multiple nodes need to agree on a single value. The network is unreliable: messages can be dropped, reordered, or delayed arbitrarily. Nodes can crash at any time. There is no shared clock.</p>
<p>A minority of nodes can fail; the protocol must still make progress. The protocol must also be safe: once a value is chosen, no future round can choose a different value.</p>
<p>This is the consensus problem. Two-Phase Commit solves it only when the coordinator doesn&rsquo;t fail. Paxos solves it even when any minority of nodes can fail, including the &ldquo;coordinator.&rdquo;</p>
<h2 id="roles">Roles</h2>
<p>Any node can play any role (often all three simultaneously in practice):</p>
<ul>
<li><strong>Proposer</strong> &ndash; initiates a round, trying to get a value agreed upon</li>
<li><strong>Acceptor</strong> &ndash; votes on proposals; the safety machinery lives here</li>
<li><strong>Learner</strong> &ndash; learns what value was finally chosen, acts on it</li>
</ul>
<h2 id="the-protocol--two-phases">The Protocol &ndash; Two Phases</h2>
<h3 id="phase-1-prepare--promise">Phase 1: Prepare / Promise</h3>
<ol>
<li>
<p>The proposer picks a <strong>proposal number</strong> <code>n</code>. It must be unique across all proposers and higher than any proposal number the proposer has seen before. (Usually implemented with <code>(timestamp, node_id)</code> tuples or a monotonic counter stored durably.)</p>
</li>
<li>
<p>Sends <code>PREPARE(n)</code> to a quorum of acceptors (any majority).</p>
</li>
<li>
<p>Each acceptor that receives <code>PREPARE(n)</code>:</p>
<ul>
<li>If <code>n</code> is higher than any PREPARE it has responded to: replies <code>PROMISE(n)</code>, committing to <strong>reject any future PREPARE or ACCEPT with a lower proposal number</strong></li>
<li>If <code>n</code> is not higher: ignores or rejects the request</li>
<li>If it has already accepted a value in a prior round, it includes that <code>(accepted_n, accepted_value)</code> in the promise</li>
</ul>
</li>
</ol>
<h3 id="phase-2-accept--accepted">Phase 2: Accept / Accepted</h3>
<ol>
<li>If the proposer receives PROMISE from a quorum:
<ul>
<li>If any promise included an <code>(accepted_n, accepted_value)</code>, the proposer <strong>must use the value with the highest <code>accepted_n</code></strong> &ndash; it cannot freely choose its own value</li>
<li>If no promise included an accepted value, the proposer can propose any value it wants</li>
</ul>
</li>
<li>Sends <code>ACCEPT(n, value)</code> to the quorum</li>
<li>Each acceptor that receives <code>ACCEPT(n, value)</code>:
<ul>
<li>If it hasn&rsquo;t promised to ignore <code>n</code>: writes the value and replies <code>ACCEPTED(n, value)</code></li>
<li>If it has made a promise with a higher number: ignores or rejects</li>
</ul>
</li>
<li>Once a quorum of acceptors reply <code>ACCEPTED</code> with the same <code>n</code> → the value is <strong>chosen</strong></li>
</ol>





<pre tabindex="0"><code>Proposer          Acceptor A    Acceptor B    Acceptor C
   |-- PREPARE(5) --&gt;|             |             |
   |-- PREPARE(5) ---|------------&gt;|             |
   |&lt;-- PROMISE(5) --|             |             |
   |&lt;-- PROMISE(5) --|-------------|             |
   |-- ACCEPT(5,v) -&gt;|             |             |
   |-- ACCEPT(5,v) --|------------&gt;|             |
   |&lt;-- ACCEPTED ---|              |             |
   |&lt;-- ACCEPTED ---|--------------|             |
      [value v is chosen, Acceptor C was unreachable but quorum achieved]</code></pre><h2 id="why-its-correct">Why It&rsquo;s Correct</h2>
<p><strong>Safety (no two values can be chosen):</strong> Suppose value <code>v</code> was chosen at proposal <code>n</code> &ndash; meaning a quorum Q1 accepted it. Any later proposer <code>n' &gt; n</code> that runs Phase 1 will receive promises from a quorum Q2. Q1 and Q2 must overlap (both are majorities). The overlapping acceptor already accepted <code>v</code> and includes <code>(n, v)</code> in its promise. The Phase 2 rule forces the new proposer to use <code>v</code> (since it&rsquo;s the highest accepted value). So no different value can ever be chosen.</p>
<p><strong>Liveness (the protocol makes progress):</strong> As long as a majority of nodes are reachable and a single stable proposer drives the protocol, a value will eventually be chosen. The protocol doesn&rsquo;t guarantee progress if proposers keep competing &ndash; see the livelock problem below.</p>
<h2 id="the-gotchas">The Gotchas</h2>
<h3 id="livelock-with-competing-proposers">Livelock with Competing Proposers</h3>
<p>Two proposers P1 and P2 can keep preempting each other indefinitely:</p>





<pre tabindex="0"><code>P1 sends PREPARE(5), gets promises from quorum
P2 sends PREPARE(6) before P1 can reach ACCEPT -- quorum promises ignore n=5
P1&#39;s ACCEPT(5, v) is rejected -- quorum promised to ignore anything &lt; 6
P1 increments to PREPARE(7), gets new promises
P2&#39;s ACCEPT(6, v) is now rejected
... loops forever</code></pre><p>The fix is a <strong>distinguished proposer</strong> (leader): only one node proposes at a time. This is what Multi-Paxos formalizes &ndash; elect a stable leader, suppress all other proposers, and the livelock disappears. It&rsquo;s not part of basic Paxos; it&rsquo;s an add-on that every production implementation requires.</p>
<h3 id="single-value-vs-log-multi-paxos">Single-Value vs. Log: Multi-Paxos</h3>
<p>Basic Paxos agrees on one value. Real systems need to agree on a sequence of commands &ndash; a log. Multi-Paxos runs Paxos for each log slot, but with an optimization: once a stable leader exists, you can skip Phase 1 for subsequent slots. You pay the two-round-trip cost once per leader election, then only Phase 2 for every log entry.</p>
<p>The problem: Multi-Paxos is not a formal protocol. It&rsquo;s described as an optimization on basic Paxos, and different implementations fill in the details differently. Log gaps are allowed &ndash; a slot can be accepted out of order. Cluster membership changes are not specified. Leader election is not specified. This leaves a large surface area for subtle bugs.</p>
<h3 id="its-hard-to-implement-correctly">It&rsquo;s Hard to Implement Correctly</h3>
<p>&ldquo;Paxos Made Simple&rdquo; describes the algorithm clearly. Getting from there to a correct implementation in a production system requires handling: durability of acceptor state across crashes (promise/accept must be persisted before responding), leader election, log gap handling, log compaction, cluster membership changes, client request routing, and linearizable reads. None of these are in the paper.</p>
<p>Chubby&rsquo;s authors wrote that their Paxos implementation took years to get right and required multiple rewrites. This is why Raft was created.</p>
<h2 id="paxos-vs-raft">Paxos vs. Raft</h2>
<p>The safety properties are equivalent. The differences are implementation surface:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Paxos (Multi-Paxos)</th>
          <th>Raft</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Log gaps</strong></td>
          <td>Allowed</td>
          <td>Not allowed &ndash; sequential only</td>
      </tr>
      <tr>
          <td><strong>Leader</strong></td>
          <td>Distinguished proposer, informally defined</td>
          <td>Formal, enforced by protocol</td>
      </tr>
      <tr>
          <td><strong>Config changes</strong></td>
          <td>Not specified</td>
          <td>Joint consensus built in</td>
      </tr>
      <tr>
          <td><strong>Understandability</strong></td>
          <td>Notoriously hard</td>
          <td>Designed to be teachable</td>
      </tr>
      <tr>
          <td><strong>Implementations</strong></td>
          <td>Chubby, Zab (ZooKeeper), Spanner</td>
          <td>etcd, CockroachDB, TiKV, Consul</td>
      </tr>
  </tbody>
</table>
<p>Same fault tolerance: both tolerate <code>⌊(N-1)/2⌋</code> node failures.</p>
<h2 id="where-paxos-is-still-used">Where Paxos Is Still Used</h2>
<ul>
<li><strong>Google Chubby</strong> &ndash; distributed lock service. Underpins Bigtable and GFS for leader election and metadata coordination.</li>
<li><strong>Apache ZooKeeper</strong> &ndash; uses Zab (ZooKeeper Atomic Broadcast), which is a Paxos variant with primary-order delivery guarantees added. Powers Kafka, HBase, and Hadoop ecosystem coordination.</li>
<li><strong>Google Spanner</strong> &ndash; Paxos per shard for cross-replica replication. Combined with TrueTime for external consistency.</li>
</ul>
<p>New systems almost universally choose Raft. The guarantees are equivalent and the implementation surface is much smaller. Paxos matters because it&rsquo;s what everything was built on, and because understanding what it leaves unspecified is the clearest way to understand why the distributed systems field spent a decade building on top of it before someone sat down and wrote a protocol that was complete enough to implement from scratch.</p>
]]></content:encoded>
    </item>
    <item>
      <title>MongoDB Connection Pool Exhaustion: Diagnosis, Fixes, and SDAM</title>
      <link>https://KTS-o7.github.io/bear/systems/connection_pool_exhaustion/</link>
      <pubDate>Sat, 06 Dec 2025 12:00:00 +0000</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/connection_pool_exhaustion/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Why increasing maxPoolSize breaks your database.&lt;/strong&gt; A deep dive into MongoDB connection pooling, SDAM, WiredTiger tickets, and preventing transaction pinning.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;&#xA;&lt;p&gt;In the MongoDB ecosystem, the &amp;ldquo;connection pool&amp;rdquo; is often treated as a set-and-forget configuration within &lt;code&gt;MongoClient&lt;/code&gt;. However, when latency spikes or the database becomes unresponsive during high traffic, the pool is usually the first mechanism to break.&lt;/p&gt;&#xA;&lt;p&gt;Unlike relational databases that often sit behind proxies like PgBouncer, MongoDB drivers are &amp;ldquo;smart&amp;rdquo;—they maintain direct connections to multiple nodes in a Replica Set or Sharded Cluster. This complexity means exhaustion isn&amp;rsquo;t just about running out of sockets; it&amp;rsquo;s about topology monitoring, transaction pinning, and the brutal reality of the thread-per-connection model on the server side.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>Why increasing maxPoolSize breaks your database.</strong> A deep dive into MongoDB connection pooling, SDAM, WiredTiger tickets, and preventing transaction pinning.</p>
</blockquote>
<h2 id="introduction">Introduction</h2>
<p>In the MongoDB ecosystem, the &ldquo;connection pool&rdquo; is often treated as a set-and-forget configuration within <code>MongoClient</code>. However, when latency spikes or the database becomes unresponsive during high traffic, the pool is usually the first mechanism to break.</p>
<p>Unlike relational databases that often sit behind proxies like PgBouncer, MongoDB drivers are &ldquo;smart&rdquo;—they maintain direct connections to multiple nodes in a Replica Set or Sharded Cluster. This complexity means exhaustion isn&rsquo;t just about running out of sockets; it&rsquo;s about topology monitoring, transaction pinning, and the brutal reality of the thread-per-connection model on the server side.</p>
<h2 id="how-mongodb-connects-and-executes-queries">How MongoDB Connects and Executes Queries</h2>
<p>Before diving into pool exhaustion, it&rsquo;s worth understanding the lifecycle of a MongoDB connection and query. The process is more involved than a simple socket open/close.</p>
<h3 id="connection-establishment">Connection Establishment</h3>
<p>When your application calls <code>MongoClient.connect()</code>, the driver doesn&rsquo;t immediately open a socket. Instead, it:</p>
<ol>
<li>
<p><strong>TCP Socket Creation:</strong> Establishes a TCP/IP connection to the MongoDB server (default port 27017). This is a standard three-way handshake at the network layer.</p>
</li>
<li>
<p><strong>Wire Protocol Handshake:</strong> The client sends a <code>hello</code> command (formerly <code>isMaster</code> in older versions) to the server. This serves multiple purposes:</p>
<ul>
<li>Verifies the server is a MongoDB instance</li>
<li>Retrieves server capabilities (wire protocol version, supported authentication mechanisms)</li>
<li>In MongoDB 4.4+, can include <code>speculativeAuthenticate</code> to start authentication concurrently, reducing latency</li>
</ul>
</li>
<li>
<p><strong>Authentication:</strong> If credentials are provided, the client authenticates using mechanisms like SCRAM (Salted Challenge Response Authentication Mechanism):</p>
<ul>
<li>Client sends <code>saslStart</code> with chosen mechanism and initial payload</li>
<li>Server responds with a challenge</li>
<li>Client computes and sends proof derived from the challenge</li>
<li>Server verifies and grants access</li>
</ul>
</li>
</ol>
<p>This entire process—TCP handshake, TLS negotiation (if enabled), handshake, and authentication—typically takes 10-50ms depending on network latency and encryption overhead.</p>
<h3 id="query-execution-flow">Query Execution Flow</h3>
<p>Once authenticated, the connection is ready for operations. When you execute a query:</p>
<ol>
<li>
<p><strong>Connection Checkout:</strong> The driver checks out a connection from the pool (or waits if none are available—this is where exhaustion manifests).</p>
</li>
<li>
<p><strong>Message Construction:</strong> The query is serialized into MongoDB&rsquo;s wire protocol format. Modern MongoDB uses the <code>OP_MSG</code> opcode (introduced in 3.6), which includes:</p>
<ul>
<li>Standard message header (request ID, response-to ID)</li>
<li>Flags indicating message properties</li>
<li>Sections containing the actual command/query data</li>
<li>Optional checksum for integrity</li>
</ul>
</li>
<li>
<p><strong>Network Transmission:</strong> The message is sent over the TCP socket to the server.</p>
</li>
<li>
<p><strong>Server Processing:</strong> The <code>mongod</code> process:</p>
<ul>
<li>Receives the message on a dedicated thread (in the thread-per-connection model)</li>
<li>Parses the query</li>
<li>Plans execution strategy</li>
<li>Accesses data through WiredTiger (subject to ticket limits)</li>
<li>Creates a cursor for read operations</li>
</ul>
</li>
<li>
<p><strong>Response:</strong> The server sends results back over the same connection. For large result sets, the driver uses cursors to fetch documents in batches (default batch size is 101 documents).</p>
</li>
<li>
<p><strong>Connection Return:</strong> After the operation completes, the connection is returned to the pool for reuse—unless it&rsquo;s pinned to a transaction (see &ldquo;The Specific Pathology: Pinning&rdquo; below).</p>
</li>
</ol>
<h3 id="why-this-matters-for-pooling">Why This Matters for Pooling</h3>
<p>Each of these steps has latency and resource costs. The connection establishment overhead (10-50ms) is why pooling exists: reusing connections avoids paying this cost for every operation. However, the server-side thread allocation means each connection has a real memory and scheduling cost on <code>mongod</code>, which is why blindly increasing pool size backfires.</p>
<h2 id="connection-pool-internals">Connection Pool Internals</h2>
<p>Understanding how the pool manages connections internally is crucial for diagnosing exhaustion. The pool maintains connections in different states:</p>
<h3 id="connection-states">Connection States</h3>
<ol>
<li>
<p><strong>Idle:</strong> Connections that are established and authenticated but not currently in use. These sit in the pool, ready for immediate checkout. They consume server-side resources (thread + memory) but are available for operations.</p>
</li>
<li>
<p><strong>Checked Out:</strong> Connections actively executing an operation. When you call <code>collection.find()</code>, the driver checks out a connection, performs the operation, then returns it to idle—unless it&rsquo;s pinned (see &ldquo;The Specific Pathology: Pinning&rdquo;).</p>
</li>
<li>
<p><strong>Pending:</strong> Connections in the process of being established (TCP handshake, authentication). These don&rsquo;t count toward <code>maxPoolSize</code> until fully established.</p>
</li>
<li>
<p><strong>Closed:</strong> Connections that have been terminated, either due to errors, idle timeout, or explicit closure.</p>
</li>
</ol>
<h3 id="the-wait-queue">The Wait Queue</h3>
<p>When all connections are checked out and the pool has reached <code>maxPoolSize</code>, new requests enter a <strong>wait queue</strong>. This queue has its own limits:</p>
<ul>
<li><strong>waitQueueTimeoutMS:</strong> How long a request waits for a connection before throwing <code>ConnectionPoolCheckoutFailed</code>. Default is 0 (infinite wait—dangerous in production). <strong>Always set this</strong> to fail fast rather than hang indefinitely.</li>
</ul>
<p>If the wait queue fills up, your application starts failing requests. This is often the first symptom of pool exhaustion, not the root cause.</p>
<h3 id="connection-lifecycle">Connection Lifecycle</h3>
<p>The connection lifecycle follows these states and transitions:</p>





<pre tabindex="0"><code>[Not Created]
    │
    │ connect()
    ▼
[Pending] ── TCP handshake, authentication ──► [Idle]
    │                                              │
    │                                              │ Driver request
    │                                              ▼
    │                                        [Checked Out]
    │                                              │
    │                                              │ Operation complete
    │                                              │
    │                                              ▼
    │                                          [Idle] ◄──┐
    │                                              │     │
    │                                              │     │ Return to pool
    │                                              │     │
    │                                              │     │
    │                                              │ startTransaction()
    │                                              ▼     │
    │                                          [Pinned] ──┘
    │                                              │
    │                                              │ ʕ•ᴥ•ʔ DANGEROUS:
    │                                              │ Exclusive to session
    │                                              │ until commit/abort
    │                                              │
    │                                              │ commit/abortTransaction()
    │                                              ▼
    │                                          [Idle]
    │                                              │
    │                                              │ maxIdleTimeMS reached
    │                                              ▼
    │                                          [Closed]
    │                                              │
    └──────────────────────────────────────────────┘</code></pre><p><strong>State Descriptions:</strong></p>
<ul>
<li><strong>Pending:</strong> Connection is being established (TCP handshake, authentication)</li>
<li><strong>Idle:</strong> Connection is established and available in the pool for immediate use</li>
<li><strong>Checked Out:</strong> Connection is actively executing an operation</li>
<li><strong>Pinned:</strong> Connection is reserved for a transaction (removed from pool until transaction completes)</li>
<li><strong>Closed:</strong> Connection has been terminated</li>
</ul>
<p>The pool lazily creates connections up to <code>maxPoolSize</code>. If <code>minPoolSize &gt; 0</code>, it pre-warms that many connections at startup. Connections idle longer than <code>maxIdleTimeMS</code> are closed to free server resources.</p>
<h2 id="sdam-the-hidden-connection-overhead">SDAM: The Hidden Connection Overhead</h2>
<p>MongoDB drivers implement <strong>SDAM</strong> (Server Discovery and Monitoring), which maintains connections to <em>all</em> nodes in a Replica Set or Sharded Cluster, not just the primary.</p>
<h3 id="how-sdam-works">How SDAM Works</h3>
<ol>
<li>
<p><strong>Initial Discovery:</strong> On <code>MongoClient.connect()</code>, the driver connects to seed addresses and sends <code>hello</code> commands to discover all replica set members.</p>
</li>
<li>
<p><strong>Continuous Monitoring:</strong> Every <code>heartbeatFrequencyMS</code> (default 10 seconds), the driver sends <code>hello</code> to each known node to:</p>
<ul>
<li>Detect role changes (primary elections, step-downs)</li>
<li>Check server capabilities</li>
<li>Update topology description</li>
</ul>
</li>
<li>
<p><strong>Topology Events:</strong> When a primary election occurs, the driver:</p>
<ul>
<li>Closes connections to the old primary</li>
<li>Opens connections to the new primary</li>
<li>Potentially drains and recreates pools</li>
</ul>
</li>
</ol>
<h3 id="the-connection-multiplier">The Connection Multiplier</h3>
<p>If you have a 3-node replica set and set <code>maxPoolSize=50</code>, you&rsquo;re not creating 50 connections—you&rsquo;re creating up to <strong>150 connections</strong> (50 per node). SDAM maintains separate pools for each server in the topology.</p>
<p>During a primary election, the driver may temporarily exceed this as it transitions between nodes. If you&rsquo;re running 20 app instances, that&rsquo;s 3,000 potential connections during failover, even if only one node is serving traffic.</p>
<p><strong>Takeaway:</strong> Your <code>maxPoolSize</code> is per-server, not per-client. In replica sets, multiply by the number of nodes you&rsquo;re monitoring.</p>
<h2 id="connection-pool-configuration-options">Connection Pool Configuration Options</h2>
<p>Here are the key options that affect pool behavior:</p>
<ul>
<li>
<p><strong>maxPoolSize:</strong> Maximum connections per server. Default 100. This is the most commonly misconfigured setting.</p>
</li>
<li>
<p><strong>minPoolSize:</strong> Minimum idle connections to maintain. Default 0. Setting this &gt; 0 pre-warms the pool but consumes server resources even when idle.</p>
</li>
<li>
<p><strong>maxIdleTimeMS:</strong> Close idle connections after this duration. Default 0 (never close). Useful in serverless where you want aggressive cleanup.</p>
</li>
<li>
<p><strong>waitQueueTimeoutMS:</strong> How long to wait for a connection before failing. Default 0 (wait forever—dangerous in production). <strong>Always set this</strong> to fail fast rather than hang indefinitely.</p>
</li>
<li>
<p><strong>connectTimeoutMS:</strong> Timeout for initial connection establishment. Default 30 seconds.</p>
</li>
<li>
<p><strong>socketTimeoutMS:</strong> Timeout for individual operations. Default 0 (no timeout). Different from connection checkout timeout.</p>
</li>
<li>
<p><strong>heartbeatFrequencyMS:</strong> How often SDAM checks server status. Default 10 seconds. Lower values detect failovers faster but increase network overhead.</p>
</li>
<li>
<p><strong>TCP Keepalive:</strong> Often overlooked. If your app sits behind a firewall or load balancer (e.g., AWS Network Load Balancer) with a 350s idle timeout, and your pool has <code>maxIdleTimeMS</code> &gt; 350s, the LB will silently drop the connection. The driver won&rsquo;t know until it tries to write to the socket and fails. <strong>Important:</strong> <code>socketTimeoutMS</code> does not send keepalive probes. <strong>Best Practice:</strong> Set <code>maxIdleTimeMS</code> to be slightly lower than your infrastructure&rsquo;s timeout (e.g., 120000ms). On Linux, also tune <code>net.ipv4.tcp_keepalive_time</code> to be less than the cloud provider&rsquo;s timeout (usually &lt; 120 seconds).</p>
</li>
</ul>
<p><strong>Example Configuration (Node.js):</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">maxPoolSize</span><span class="o">:</span> <span class="mi">10</span><span class="p">,</span>              <span class="c1">// Small pool for 16-core DB
</span></span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">minPoolSize</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>               <span class="c1">// Pre-warm 2 connections
</span></span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">maxIdleTimeMS</span><span class="o">:</span> <span class="mi">30000</span><span class="p">,</span>         <span class="c1">// Close idle after 30s
</span></span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">waitQueueTimeoutMS</span><span class="o">:</span> <span class="mi">5000</span><span class="p">,</span>     <span class="c1">// Fail after 5s wait
</span></span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">connectTimeoutMS</span><span class="o">:</span> <span class="mi">10000</span><span class="p">,</span>      <span class="c1">// 10s connection timeout
</span></span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nx">socketTimeoutMS</span><span class="o">:</span> <span class="mi">45000</span><span class="p">,</span>       <span class="c1">// 45s operation timeout
</span></span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="nx">heartbeatFrequencyMS</span><span class="o">:</span> <span class="mi">10000</span>   <span class="c1">// Check every 10s
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h2 id="the-cost-of-a-connection-in-mongod">The Cost of a Connection in mongod</h2>
<p>While MongoDB has moved toward a more asynchronous networking model in recent versions, historically (and effectively, for resource planning), it relies on a synchronous thread-per-connection model.</p>
<p>When your application opens a new connection:</p>
<ol>
<li><strong>Network:</strong> TCP handshake + TLS negotiation (CPU intensive).</li>
<li><strong>Authentication:</strong> SCRAM or X.509 handshakes occur.</li>
<li><strong>Memory:</strong> The <code>mongod</code> process allocates stack memory (typically ~1MB) for the thread handling that connection.</li>
<li><strong>Context:</strong> The internal <code>serviceExecutor</code> must schedule this thread.</li>
</ol>
<p>If you set <code>maxPoolSize=100</code> on 50 Kubernetes pods, you are allowing 5,000 potential concurrent threads on the Primary. Before you hit hardware limits, you will likely hit <strong>WiredTiger ticket exhaustion</strong>.</p>
<h2 id="the-math-sizing-for-wiredtiger">The Math: Sizing for WiredTiger</h2>
<p>The naive approach is to increase <code>maxPoolSize</code> when you see <code>ConnectionPoolCheckoutFailed</code> errors. This is usually wrong.</p>
<p>WiredTiger (the storage engine) uses &ldquo;tickets&rdquo; to control concurrency. In MongoDB versions prior to 7.0, there are 128 read tickets and 128 write tickets by default. MongoDB 7.0+ uses dynamic ticketing that adjusts based on workload, but still caps at 128 per type. <strong>Note on MongoDB 7.0+:</strong> While the hard cap remains 128 tickets, the dynamic algorithm often lowers this limit (e.g., to 60 or 80) in real-time to preserve latency. This means you might see queuing before you hit 128 concurrent active queries. If 1,000 connections try to read simultaneously, 872 of them are just queuing inside the kernel, consuming RAM but doing zero work.</p>
<p>The optimal pool size formula remains tied to hardware capability, not traffic volume:</p>
$$
PoolSize \approx \frac{\text{CoreCount} \times 2}{\text{AppInstances}}
$$<p>If your database has 16 cores and you have 20 application instances, a <code>maxPoolSize</code> of 100 is overkill. You might actually achieve <em>higher</em> throughput with a pool size of 2 or 3 per instance, preventing the database from spending all its cycles on context switching.</p>
<p><strong>Note:</strong> The <code>(Cores * 2)</code> rule applies strictly to the <em>active</em> executing threads. You can have a larger pool to handle network jitter, but <code>waitQueueTimeoutMS</code> is your safety valve. If you need 100 connections to saturate the CPU, your queries are likely too slow (unindexed). Fix the queries, don&rsquo;t inflate the pool.</p>
<p><strong>Workload Considerations:</strong></p>
<ul>
<li><strong>CPU-Bound Workloads:</strong> The formula above applies directly. Each active connection should map to an executing thread.</li>
<li><strong>IO-Bound Workloads:</strong> If the working set doesn&rsquo;t fit in RAM and disk I/O is the bottleneck, a slightly larger pool <em>might</em> help hide I/O latency, though typically <code>minPoolSize</code> is more relevant here to avoid establishing connections during I/O spikes.</li>
<li><strong>Atlas Proxy / Serverless:</strong> If using <strong>MongoDB Atlas Serverless</strong> or the <strong>Atlas Proxy</strong>, the connection limits are handled differently (via a proxy layer), and the &ldquo;100 limit&rdquo; on the client side is less about server stress and more about client-side throttling.</li>
</ul>
<h2 id="the-specific-pathology-pinning">The Specific Pathology: &ldquo;Pinning&rdquo;</h2>
<p>Since you are versed in Mongo, you know the drivers implement the <strong>SDAM</strong> (Server Discovery and Monitoring) specification. But the most dangerous feature regarding pooling is <strong>Client-Side Operation Pinning</strong>, specifically with Transactions.</p>
<p>In a standard read/write, the driver grabs a connection, executes, and returns it.
In a Transaction (Multi-Document ACID):</p>
<ol>
<li><code>session.startTransaction()</code> is called.</li>
<li>The driver <strong>pins</strong> a connection to that session.</li>
<li><strong>That connection is removed from the pool</strong> and reserved exclusively for this session until <code>commitTransaction</code> or <code>abortTransaction</code> is called.</li>
</ol>
<h3 id="the-exhaustion-scenario">The Exhaustion Scenario</h3>
<p>If you have <code>maxPoolSize=50</code> and an API endpoint that takes 2 seconds to execute some logic <em>inside</em> a transaction, you can only serve 25 requests per second. The 26th request will wait. If the logic hangs (e.g., waiting for a third-party HTTP call), you will drain the pool instantly, causing a cascading failure across the app.</p>
<blockquote>
<p><strong>ʕ•ᴥ•ʔ Critical Rule:</strong> Never perform network I/O or long computations inside a MongoDB transaction. Transactions should be atomic, fast, and only contain database operations.</p>
</blockquote>
<h2 id="practical-examples-the-right-way-vs-the-wrong-way">Practical Examples: The Right Way vs. The Wrong Way</h2>
<h3 id="proper-client-initialization">Proper Client Initialization</h3>
<p><strong>Node.js - Express (Correct):</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// app.js - Initialize once at startup
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">MongoClient</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;mongodb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">let</span> <span class="nx">client</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">let</span> <span class="nx">db</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">initDatabase</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">maxPoolSize</span><span class="o">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">waitQueueTimeoutMS</span><span class="o">:</span> <span class="mi">5000</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kr">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">connect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">db</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="s1">&#39;myapp&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Database connected&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// Initialize before starting server
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nx">initDatabase</span><span class="p">().</span><span class="nx">then</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1">// Use the same client instance everywhere
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/users&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="kr">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">collection</span><span class="p">(</span><span class="s1">&#39;users&#39;</span><span class="p">).</span><span class="nx">find</span><span class="p">({}).</span><span class="nx">toArray</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">users</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><strong>Node.js - Express (Wrong - Creates New Pool Per Request):</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// ʕ•̀ω•́ʔ DON&#39;T DO THIS
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/users&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">);</span>  <span class="c1">// New pool every request!
</span></span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kr">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">connect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="kr">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nx">db</span><span class="p">(</span><span class="s1">&#39;myapp&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kr">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">collection</span><span class="p">(</span><span class="s1">&#39;users&#39;</span><span class="p">).</span><span class="nx">find</span><span class="p">({}).</span><span class="nx">toArray</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="kr">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>  <span class="c1">// Closes pool, but too late
</span></span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">users</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><strong>Python - Gunicorn (Correct):</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># app.py - Create client after fork</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">pymongo</span> <span class="kn">import</span> <span class="n">MongoClient</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">db</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">on_starting</span><span class="p">(</span><span class="n">server</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1"># This runs in the master process</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">pass</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">when_ready</span><span class="p">(</span><span class="n">server</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># This runs in each worker after fork</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">global</span> <span class="n">client</span><span class="p">,</span> <span class="n">db</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span><span class="n">uri</span><span class="p">,</span> <span class="n">maxPoolSize</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s1">&#39;myapp&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/users&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="k">def</span> <span class="nf">get_users</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">users</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">users</span><span class="o">.</span><span class="n">find</span><span class="p">({}))</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">users</span><span class="p">)</span></span></span></code></pre></div><p><strong>Python - Gunicorn (Wrong - Fork-Unsafe):</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ʕ•̀ω•́ʔ DON&#39;T DO THIS</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">pymongo</span> <span class="kn">import</span> <span class="n">MongoClient</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># Created in master process - NOT fork-safe!</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s1">&#39;myapp&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/users&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">def</span> <span class="nf">get_users</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># Child processes share parent&#39;s file descriptors = deadlocks</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1"># This often results in AutoReconnect errors or </span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># &#39;file descriptor bad&#39; errors because the socket </span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1"># is shared across memory boundaries</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">users</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">users</span><span class="o">.</span><span class="n">find</span><span class="p">({}))</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">users</span><span class="p">)</span></span></span></code></pre></div><p><strong>Java - Spring Boot:</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nl">spring</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="n">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">mongodb</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span><span class="n">uri</span><span class="p">:</span><span class="w"> </span><span class="s">&#34;mongodb://user:pass@host1:27017,host2:27017/?replicaSet=myRS&amp;maxPoolSize=10&amp;minPoolSize=2&amp;maxIdleTimeMS=30000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// Or if using a Configurer Bean (safe way to customize without breaking auto-config)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nd">@Configuration</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">MongoConfig</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nd">@Bean</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">MongoClientSettingsBuilderCustomizer</span><span class="w"> </span><span class="nf">customizer</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">builder</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">builder</span><span class="p">.</span><span class="na">applyToConnectionPoolSettings</span><span class="p">(</span><span class="n">pool</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">            </span><span class="n">pool</span><span class="p">.</span><span class="na">maxSize</span><span class="p">(</span><span class="n">10</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">            </span><span class="n">pool</span><span class="p">.</span><span class="na">maxWaitTime</span><span class="p">(</span><span class="n">5</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">SECONDS</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>The Health Check Trap:</strong></p>
<blockquote>
<p><strong>ʕ•ᴥ•ʔ Warning:</strong> Do not create a new <code>MongoClient</code> inside your Kubernetes readiness/liveness probe. This is a classic DOS attack on your own database. The probe runs every 10s; if it creates a new connection each time without proper closure, you will exhaust the server limits in minutes. Use the singleton client to run a lightweight command like <code>db.command({ ping: 1 })</code>.</p>
</blockquote>
<h3 id="monitoring-pool-metrics">Monitoring Pool Metrics</h3>
<p><strong>Node.js - Command Monitoring:</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">MongoClient</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;mongodb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">monitorCommands</span><span class="o">:</span> <span class="kc">true</span>  <span class="c1">// Enable command monitoring
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;commandStarted&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Command:&#39;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">commandName</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;commandSucceeded&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Duration:&#39;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">duration</span><span class="p">,</span> <span class="s1">&#39;ms&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">// For connection pool metrics, use pool monitoring events
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;connectionCheckedOut&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">duration</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// Checkout took &gt; 100ms
</span></span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="s1">&#39;Slow connection checkout:&#39;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">duration</span><span class="p">,</span> <span class="s1">&#39;ms&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nx">client</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;commandFailed&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">error</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="s1">&#39;ConnectionPoolCheckoutFailed&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Pool exhausted!&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><strong>Go - Pool Monitoring:</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="s">&#34;go.mongodb.org/mongo-driver/event&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="s">&#34;go.mongodb.org/mongo-driver/mongo&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">poolMonitor</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">event</span><span class="p">.</span><span class="nx">PoolMonitor</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nx">Event</span><span class="p">:</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">evt</span><span class="w"> </span><span class="o">*</span><span class="nx">event</span><span class="p">.</span><span class="nx">PoolEvent</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="k">switch</span><span class="w"> </span><span class="nx">evt</span><span class="p">.</span><span class="nx">Type</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">ConnectionCreated</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">            </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Connection created&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">ConnectionCheckedOut</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">            </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Connection checked out&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">ConnectionCheckoutFailed</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">            </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Checkout failed:&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">evt</span><span class="p">.</span><span class="nx">Reason</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">ConnectionReturned</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">            </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Connection returned&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nx">clientOptions</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">options</span><span class="p">.</span><span class="nf">Client</span><span class="p">().</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nf">ApplyURI</span><span class="p">(</span><span class="nx">uri</span><span class="p">).</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nf">SetPoolMonitor</span><span class="p">(</span><span class="nx">poolMonitor</span><span class="p">).</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span><span class="nf">SetMaxPoolSize</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nx">client</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">mongo</span><span class="p">.</span><span class="nf">Connect</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">TODO</span><span class="p">(),</span><span class="w"> </span><span class="nx">clientOptions</span><span class="p">)</span></span></span></code></pre></div><h2 id="driver-nuances--pitfalls">Driver Nuances &amp; Pitfalls</h2>
<h3 id="nodejs-mongoose--native-driver">Node.js (Mongoose / Native Driver)</h3>
<p>The Node driver is asynchronous, but the pool logic is strict.</p>
<ul>
<li><strong>The Singleton Mistake:</strong> A common pattern in serverless or improper Express setup is calling <code>MongoClient.connect()</code> inside the route handler. This creates a new pool for every request, rapidly hitting the Atlas connection limit (e.g., 1,500 connections on M10). Always initialize the client <em>once</em> at startup.</li>
<li><strong>Unified Topology:</strong> The Node.js driver (v4.0+) uses Unified Topology by default (it&rsquo;s the only option). This keeps a background monitoring thread that can detect step-downs. If your pool churns during a primary election, check if your <code>heartbeatFrequencyMS</code> is tuned correctly.</li>
</ul>
<h3 id="python-pymongo">Python (PyMongo)</h3>
<ul>
<li><strong>Fork Safety:</strong> <code>MongoClient</code> is not fork-safe. If you use <code>gunicorn</code> or <code>uwsgi</code> with forking, you <strong>must</strong> create the client <em>after</em> the fork (e.g., inside the worker process initialization), or you will end up with child processes trying to use the parent&rsquo;s file descriptors, leading to deadlocks and obscure socket errors. This often manifests as <code>AutoReconnect</code> errors or &ldquo;file descriptor bad&rdquo; errors.</li>
<li><strong>Wait Queue:</strong> In PyMongo 4.0+, <code>waitQueueMultiple</code> was removed. Use <code>waitQueueTimeoutMS</code> to control how long threads wait for connections. Limit your application&rsquo;s thread pool size to control concurrency.</li>
</ul>
<h3 id="java-spring-boot--mongodb-java-driver">Java (Spring Boot / MongoDB Java Driver)</h3>
<ul>
<li><strong>Automatic Pooling:</strong> The <code>MongoTemplate</code> handles pooling automatically. Ensure <code>spring.data.mongodb.database</code> settings do not override defaults with aggressive timeouts. The Java driver&rsquo;s default <code>maxPoolSize</code> is 100, which is often too high for production.</li>
</ul>
<h3 id="go-mongo-go-driver">Go (mongo-go-driver)</h3>
<ul>
<li>The Go driver respects <code>context</code>. If your <code>Connect</code> or <code>Find</code> context times out, the connection might be closed or returned to the pool depending on the stage.</li>
<li>Monitor <code>PoolEvent</code> via the <code>SetPoolMonitor</code> option to visualize exactly when connections are created vs. checked out.</li>
</ul>
<h2 id="serverless--atlas-implications">Serverless &amp; Atlas Implications</h2>
<p>In MongoDB Atlas, connection limits are hard tiers.</p>
<ul>
<li><strong>M0 (Free Tier):</strong> 500 connections per node</li>
<li><strong>M10:</strong> 1,500 connections per node</li>
<li><strong>M20:</strong> 3,000 connections per node</li>
<li><strong>Serverless Instances:</strong> These bill by &ldquo;Read Processing Units&rdquo; but still require connection management.</li>
</ul>
<p>If you are running on AWS Lambda or Vercel, you cannot maintain a persistent pool. Every &ldquo;warm&rdquo; container might keep a pool of 10 open. 100 concurrent Lambdas = 1,000 connections.
<strong>Solution:</strong> You typically cannot use standard pooling here. You must aggressively close connections (set <code>maxIdleTimeMS</code> very low) or use the <strong>Atlas Data API</strong> (HTTP based) instead of the TCP driver to avoid socket exhaustion.</p>
<h2 id="monitoring--diagnosis">Monitoring &amp; Diagnosis</h2>
<p>How do you know it&rsquo;s a pool issue and not a slow query?</p>
<ol>
<li>
<p><strong>Check the Server:</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">serverStatus</span><span class="p">().</span><span class="nx">connections</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// look for { &#34;current&#34;: 1200, &#34;available&#34;: 50 }
</span></span></span></code></pre></div><p>If <code>current</code> is high, your app is leaking connections or scaling too wide.</p>
</li>
<li>
<p><strong>Check the Queue:</strong>
Monitor WiredTiger ticket usage via <code>db.serverStatus().wiredTiger.concurrentTransactions</code> or connection counts. If active connections significantly exceed your pool size or WiredTiger tickets are exhausted, your latency is due to queuing, not network speed.</p>
</li>
<li>
<p><strong>Zombie Connections:</strong>
When a client crashes hard (OOM Kill) without sending a TCP FIN, the server keeps the connection open until the server-side <code>tcp_keepalive</code> times out. In high-churn Kubernetes environments, this can exhaust the server&rsquo;s file descriptors even if the <em>active</em> pool count seems low. This reinforces the need for aggressive <code>maxIdleTimeMS</code> and proper TCP keepalive configuration.</p>
</li>
<li>
<p><strong>Driver Metrics:</strong>
Enable command monitoring in your driver. If <code>connectionCheckoutTime</code> spikes while <code>commandDuration</code> (server execution time) stays low, the problem is client-side exhaustion. The database is fast; the line to get to it is slow.</p>
</li>
<li>
<p><strong>Connection Churn Rate:</strong>
Monitor the rate of new connections (<code>connectionsCreated</code> per second). If this is high (&gt;10/sec per node) while your total pool count is stable, your <code>maxIdleTimeMS</code> is likely too low. You are burning server CPU on SSL handshakes rather than reusing connections. Check <code>db.serverStatus().connections.created</code> and compare it to your connection pool turnover rate.</p>
</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>In MongoDB, &ldquo;Connection Pool Exhaustion&rdquo; is often a misnomer for &ldquo;Transaction Pinning&rdquo; or &ldquo;Overscaling.&rdquo; Because the drivers are complex state machines interacting with Replica Sets, simply bumping <code>maxPoolSize</code> masks the problem until it crashes <code>mongod</code> via memory pressure or ticket exhaustion.</p>
<p>The fix is almost always:</p>
<ol>
<li>Lower the pool size to match hardware (<code>threads &lt;= cores</code>).</li>
<li>Shorten transaction scopes to microseconds.</li>
<li>Treat the <code>MongoClient</code> as a sacred singleton.</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Why MySQL is not CA</title>
      <link>https://KTS-o7.github.io/bear/systems/why_mysql_is_not_ca/</link>
      <pubDate>Fri, 05 Sep 2025 07:07:07 +0100</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/why_mysql_is_not_ca/</guid>
      <description>&lt;hr&gt;&#xA;&lt;h2 id=&#34;motivation&#34;&gt;motivation&lt;/h2&gt;&#xA;&lt;p&gt;I was recently discussing with my CTO about databases. There was one point where we were thinking about the &lt;code&gt;CAP&lt;/code&gt; theorem.&#xA;Then he randomly mentioned that MySQL is not CA. I was surprised because I always thought that MySQL is a CA database at its core.&lt;/p&gt;&#xA;&lt;p&gt;Now I had to search and understand why MySQL is not CA.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;cap-theorem&#34;&gt;CAP theorem&lt;/h2&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://substackcdn.com/image/fetch/$s_!cOsx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869db2d6-6133-411e-96ad-cc1c52611778_800x472.png&#34; alt=&#34;CAP theorem&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;We have always been taught in Engineering that CAP is one of the most important theorems in distributed systems and databases. We just accepted it. Time to question and learn ┏ʕ •ᴥ•ʔ┛&lt;/p&gt;</description>
      <content:encoded><![CDATA[<hr>
<h2 id="motivation">motivation</h2>
<p>I was recently discussing with my CTO about databases. There was one point where we were thinking about the <code>CAP</code> theorem.
Then he randomly mentioned that MySQL is not CA. I was surprised because I always thought that MySQL is a CA database at its core.</p>
<p>Now I had to search and understand why MySQL is not CA.</p>
<hr>
<h2 id="cap-theorem">CAP theorem</h2>
<p><img src="https://substackcdn.com/image/fetch/$s_!cOsx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F869db2d6-6133-411e-96ad-cc1c52611778_800x472.png" alt="CAP theorem"></p>
<p>We have always been taught in Engineering that CAP is one of the most important theorems in distributed systems and databases. We just accepted it. Time to question and learn ┏ʕ •ᴥ•ʔ┛</p>
<p>Here is what it actually means.</p>
<p>Imagine a bank server, when you check your account balance it should not randomly change it based on the server you are hitting and it should be consistent across all nodes. But in a distributed system, network failures (partitions) can occur. The CAP theorem states that you can only guarantee 2 out of 3 properties:</p>
<ul>
<li><strong>Consistency (C)</strong>: All nodes see the same data at the same time</li>
<li><strong>Availability (A)</strong>: Every request receives a response, even if it&rsquo;s not the most recent data</li>
<li><strong>Partition Tolerance (P)</strong>: The system continues to operate despite network partitions</li>
</ul>
<p>Here are practical examples of each combination:</p>
<h3 id="ca-systems-consistent--available-but-not-partition-tolerant">CA Systems (Consistent + Available, but not Partition Tolerant)</h3>
<p><code>Example: Traditional Relational Databases (like PostgresSQL in single-node setups)</code></p>
<p>Imagine a single PostgresSQL database server in a bank. When you check your balance:</p>
<ul>
<li><strong>Consistency</strong>: Every time you query your balance, you get the exact same amount ($100.00)</li>
<li><strong>Availability</strong>: The system always responds to your requests</li>
<li><strong>Partition Tolerance</strong>: If the network connection to the database fails, you can&rsquo;t access your account</li>
</ul>
<p>This works perfectly in non-distributed environments, but fails when you need to scale across multiple data centers.</p>
<h3 id="cp-systems-consistent--partition-tolerant-but-sacrifices-availability">CP Systems (Consistent + Partition Tolerant, but sacrifices Availability)</h3>
<p><code>Example: Apache ZooKeeper or etcd</code></p>
<p>Imagine a distributed configuration management system like ZooKeeper:</p>
<ul>
<li><strong>Consistency</strong>: All nodes agree on configuration values (like which server is the leader)</li>
<li><strong>Partition Tolerance</strong>: If some nodes lose network connectivity, the remaining nodes continue operating</li>
<li><strong>Availability</strong>: During a network partition, some requests might be blocked to maintain consistency</li>
</ul>
<p>In a ZooKeeper cluster, if a majority of nodes become unreachable, the system stops accepting writes to prevent inconsistent states.</p>
<h3 id="ap-systems-available--partition-tolerant-but-sacrifices-consistency">AP Systems (Available + Partition Tolerant, but sacrifices Consistency)</h3>
<p><code>Example: Amazon DynamoDB or Cassandra</code></p>
<p>Imagine a social media platform&rsquo;s like counter system:</p>
<ul>
<li><strong>Availability</strong>: The system always responds to requests, even during network failures</li>
<li><strong>Partition Tolerance</strong>: The system continues operating across multiple data centers despite network issues</li>
<li><strong>Consistency</strong>: During partitions, you might see different like counts on different servers</li>
</ul>
<p>You might see a post with 100 likes on one page refresh, then 102 likes on the next refresh, as the systems reconcile the differences asynchronously.</p>
<hr>
<h3 id="why-mysql-is-not-ca">Why MySQL is not CA</h3>
<p>While MySQL can achieve CA in single-node deployments, it cannot be truly CA in distributed scenarios. When you add MySQL replicas or clustering solutions like MySQL Group Replication:</p>
<ul>
<li>During network partitions, MySQL must choose between consistency (rejecting writes) or availability (allowing potentially conflicting writes)</li>
<li>MySQL&rsquo;s synchronous replication can become unavailable during partitions if strict consistency is enforced</li>
<li>Traditional asynchronous replication trades consistency for availability (so it becomes AP, not CA).
Even with semi-sync replication, MySQL typically prioritizes availability over strict consistency—clients may see stale reads.</li>
</ul>
<p>It doesn’t enforce strong ACID guarantees across replicas the way PostgreSQL doe</p>
<h3 id="why-postgresql-is-considered-ca">Why PostgreSQL is considered CA</h3>
<p>Postgres in standalone mode: Definitely CA (strong ACID, always responds if node is up).</p>
<p>In replication setups:</p>
<p>PostgreSQL replication (especially with synchronous replication) prioritizes consistency first.
A commit can be acknowledged only after replicas confirm the write (if you configure synchronous replication).</p>
<p>This means if replicas aren’t reachable, Postgres will prefer not to serve stale data (sacrificing availability to preserve consistency).</p>
<p>So Postgres is effectively CA (or CP in distributed setups).</p>
<p>This is why distributed databases like CockroachDB or TiDB are often preferred for truly distributed, partition-tolerant systems.</p>
<hr>
<p>ʕ ● ᴥ ●ʔ</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
