<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" 
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Mohammed Mostafa's Blog</title>
    <description>Software Engineering, Backend Development, ASP.NET Core, Node.js, and Programming Tutorials by Mohammed Mostafa</description>
    <link>https://www.modev.me/blog</link>
    <language>en</language>
    <managingEditor>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</managingEditor>
    <webMaster>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</webMaster>
    <lastBuildDate>Thu, 17 Sep 2026 15:57:04 GMT</lastBuildDate>
    <atom:link href="https://www.modev.me/blog/rss.xml" rel="self" type="application/rss+xml"/>
    <image>
      <url>https://www.modev.me/avatar.jpg</url>
      <title>Mohammed Mostafa's Blog</title>
      <link>https://www.modev.me/blog</link>
      <width>400</width>
      <height>400</height>
    </image>
    <copyright>Copyright 2026 Mohammed Mostafa</copyright>
    <category>Technology</category>
    <category>Software Engineering</category>
    <category>Backend Development</category>
    <ttl>60</ttl>

    <item>
      <title><![CDATA[Building a Transport Protocol on Top of UDP]]></title>
      <description><![CDATA[A practical guide to reliable transport, congestion control, flow control, and connection management, using AeroUDP, a TCP-like protocol built on top of UDP in async Rust.]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/aeroudp-networking-concepts" alt="Building a Transport Protocol on Top of UDP" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>Building a Transport Protocol on Top of UDP</h1>
<p>This article explains the core networking concepts that make a reliable transport protocol work, using AeroUDP as a concrete reference implementation. AeroUDP is a reliable, ordered, congestion-controlled transport protocol layered on top of unreliable UDP, essentially a stripped-down hybrid of TCP and QUIC written in idiomatic asynchronous Rust.</p>
<p>> Source code: https://github.com/Mo7ammedd/AeroUDP</p>
<p>---</p>
<h2>Why Build a Protocol on Top of UDP?</h2>
<h3>The Transport Layer</h3>
<p>The transport layer sits between the raw network (IP) and your application. It is responsible for turning a best-effort packet delivery service into something programs can actually rely on. The two classic choices are TCP and UDP.</p>
<strong>TCP (Transmission Control Protocol):</strong>
<ul><li>Reliable, ordered, connection-oriented byte stream</li>
<li>Built-in congestion and flow control</li>
<li>Implemented in the kernel, hard to modify or experiment with</li>
</ul>
<strong>UDP (User Datagram Protocol):</strong>
<ul><li>Unreliable, unordered, connectionless datagrams</li>
<li>No congestion control, no retransmission, no ordering</li>
<li>A thin wrapper over IP, fast, minimal, and fully programmable in user space</li>
</ul>
<h3>The Middle Ground</h3>
<p>Modern protocols like QUIC (which powers HTTP/3) are built <em>on top of</em> UDP in user space, re-implementing reliability and congestion control where they can evolve quickly without kernel changes.</p>
<p>AeroUDP takes the same approach for educational and experimental purposes. It uses UDP as a dumb packet pipe and rebuilds the guarantees people expect from TCP:</p>
<ul><li>Reliable, in-order, bidirectional delivery</li>
<li>Detection and recovery from loss, duplication, reordering, and corruption</li>
<li>Sliding-window flow control</li>
<li>NewReno-style congestion control</li>
<li>RTT-driven adaptive timeouts</li>
<li>A full connection lifecycle (handshake, graceful close, reset, keepalive)</li>
</ul>
---
<h2>Packets and the Wire Format</h2>
<h3>What is a Packet Header?</h3>
<p>Before any bytes travel across a network, they must be wrapped in a <strong>header</strong>, a fixed structure of metadata that tells the receiver how to interpret the payload. The header answers questions like: <em>Which connection is this? What sequence number? What is being acknowledged? How much buffer space is available?</em></p>
<h3>AeroUDP Header</h3>
<p>Every AeroUDP datagram is exactly one UDP payload carrying one packet with a 28-byte header:</p>
<p>``<code>
+---------------+---------------+---------------+---------------+
|  magic=0xAE   |  version=1    | packet_type   |   flags       |
+---------------+---------------+---------------+---------------+
|                          conn_id (u32)                        |
|                          seq_num (u32)                        |
|                          ack_num (u32)                        |
|                          window  (u32)                        |
|         payload_len (u16)     |          reserved (u16)       |
|             checksum (u32, CRC32 over header + payload)       |
|                          payload ...                          |
+---------------------------------------------------------------+
</code>`<code></p>
<strong>Key design choices:</strong>
<ul><li><strong>Magic byte + version</strong>: lets the receiver reject garbage or mismatched protocol versions immediately.</li>
<li><strong>Big-endian fields</strong>: "network byte order", the universal convention so machines with different native byte orders agree.</li>
<li><strong>1200-byte max payload</strong>: chosen to fit inside common network MTUs (Maximum Transmission Unit) and avoid IP fragmentation.</li>
</ul>
<h3>Error Detection with Checksums</h3>
<p>Networks corrupt bits. A <strong>checksum</strong> is a small value computed over the data so the receiver can detect corruption.</p>
<strong>How AeroUDP does it:</strong>
<ul><li>The checksum field is set to zero.</li>
<li>CRC32 is computed over the entire packet (header + payload).</li>
<li>The result is written back into the checksum field.</li>
</ul>
On receipt, the process is reversed. A mismatch means the packet is silently dropped, from the protocol's perspective it never arrived, and reliability mechanisms will recover it.
<strong>Why CRC32?</strong>
<ul><li>Fast to compute in hardware and software</li>
<li>Excellent at catching burst errors</li>
<li>Not cryptographic, it detects accidental corruption, not malicious tampering</li>
</ul>
---
<h2>Reliability: Turning "Best Effort" into "Guaranteed"</h2>
<p>UDP can lose, duplicate, or reorder packets. Reliability is the machinery that hides all of that from the application.</p>
<h3>Sequence Numbers</h3>
<strong>Concept:</strong>
<ul><li>Each byte-consuming packet gets a monotonically increasing <strong>sequence number</strong>.</li>
<li>The receiver uses these to reassemble data in the correct order and to detect gaps (loss) or repeats (duplication).</li>
</ul>
<strong>In AeroUDP:</strong>
<ul><li>Each side picks a <strong>random 32-bit Initial Sequence Number (ISN)</strong> at connection time. Randomizing the ISN prevents old packets from a previous connection from being mistaken for new ones.</li>
<li>SYN, FIN, and DATA each consume exactly one sequence slot. Pure ACKs consume none.</li>
<li>Comparisons use <strong>modular 32-bit arithmetic</strong>, so the space wraps around cleanly.</li>
</ul>
<h3>Cumulative Acknowledgements (ACKs)</h3>
<strong>Concept:</strong>
An <strong>ACK</strong> tells the sender "I received everything up to here." AeroUDP uses <em>cumulative</em> ACKs: the </code>ack_num<code> field means "the next in-order sequence number I expect."
<strong>Example:</strong>
</code>`<code>
Receiver has: 1, 2, 3, 4    (missing 5)
ack_num sent: 5             (expecting 5 next)
<p>Later receives: 6, 7        (still missing 5, out of order)
ack_num stays: 5            (cannot advance past the gap)
</code>`<code></p>
<strong>Advantages of cumulative ACKs:</strong>
<ul><li>Simple and robust, a single number summarizes everything received</li>
<li>A lost ACK is harmless if a later ACK gets through (it supersedes the old one)</li>
</ul>
<strong>Disadvantage:</strong>
<ul><li>Cannot tell the sender <em>which specific</em> later packets arrived after a gap. That is what Selective ACK (SACK) solves, a feature AeroUDP intentionally omits for simplicity.</li>
</ul>
<h3>Automatic Repeat reQuest (ARQ)</h3>
<p>The general strategy of "detect loss, then resend" is called <strong>ARQ</strong>. AeroUDP implements it with a <strong>retransmission queue</strong>:</p>
<strong>How it works:</strong>
<ul><li>Every DATA packet is stored in an in-flight queue, keyed by sequence number.</li>
<li>A cumulative ACK evicts everything strictly below </code>ack_num<code>, those packets are confirmed delivered.</li>
<li>If a packet is not acknowledged in time, it is resent (with a </code>RETX<code> flag set for observability).</li>
</ul>
<strong>Example:</strong>
</code>`<code>
In-flight queue: {5, 6, 7, 8}
ACK arrives with ack_num = 7
  → evict 5 and 6 (confirmed)
  → 7 and 8 remain in flight
</code>`<code>
<p>---</p>
<h2>Round-Trip Time and Retransmission Timeouts</h2>
<h3>The Core Problem</h3>
<p>How long should a sender wait before deciding a packet was lost? Too short, and it retransmits packets that were merely slow, wasting bandwidth. Too long, and it stalls after real losses.</p>
<p>The answer is to <strong>measure</strong> the network and adapt. The <strong>Round-Trip Time (RTT)</strong> is how long a packet takes to be sent and acknowledged. The <strong>Retransmission Timeout (RTO)</strong> is derived from it.</p>
<h3>RFC 6298 Estimation</h3>
<p>AeroUDP follows the standard TCP algorithm from RFC 6298. It tracks a <strong>smoothed RTT (SRTT)</strong> and the <strong>RTT variation (RTTVAR)</strong>:</p>
</code>`<code>
First sample R:
  SRTT   = R
  RTTVAR = R / 2
<p>Each later sample R':
  RTTVAR = (1 - 1/4) <em> RTTVAR + (1/4) </em> |SRTT - R'|
  SRTT   = (1 - 1/8) <em> SRTT   + (1/8) </em> R'</p>
<p>RTO = clamp(SRTT + 4 * RTTVAR, min_rto, max_rto)
</code>`<code></p>
<strong>Why include variance?</strong>
A network with steady 50 ms RTT and one with wildly swinging 20–200 ms RTT can have the same average. Adding </code>4 * RTTVAR<code> makes the timeout generous when the network is jittery and tight when it is stable.
<h3>Karn's Algorithm</h3>
<strong>The ambiguity problem:</strong> if a packet is retransmitted and then an ACK arrives, was it acknowledging the <em>original</em> or the <em>retransmission</em>? You cannot tell, so the RTT sample is unreliable.
<strong>Karn's rule:</strong> never take an RTT sample from a retransmitted segment. AeroUDP enforces exactly this, only clean, first-try packets update SRTT.
<h3>Exponential Backoff</h3>
<p>When an RTO fires (a real timeout), AeroUDP <strong>doubles</strong> the RTO (</code>RTO *= 2<code>) before retrying. This is <strong>exponential backoff</strong>, it prevents a sender from hammering an already-congested or broken network. On the next fresh RTT sample, the backoff resets.</p>
<strong>Reference defaults:</strong>
</code>`<code>
initial_rto        = 300 ms
min_rto / max_rto  = 100 ms / 10 s
max_retries        = 12    (then the connection is torn down)
</code>`<code>
<p>---</p>
<h2>Flow Control: Don't Overwhelm the Receiver</h2>
<h3>The Problem</h3>
<p>A fast sender talking to a slow receiver (or one whose application is slow to read) will overflow the receiver's buffer, forcing it to drop data. Flow control prevents this by letting the receiver throttle the sender.</p>
<h3>Sliding Window</h3>
<strong>Concept:</strong>
The receiver advertises how much buffer space it currently has, the <strong>receive window</strong> (the </code>window<code> field in the header, measured in packets in AeroUDP). The sender must never have more unacknowledged data in flight than the window allows.
<strong>Intended sliding-window behavior:</strong>
</code>`<code>
Receiver advertises window = 8 packets
Sender may have at most 8 unacknowledged packets in flight.
<p>As the application reads and drains the buffer, the window grows.
As the buffer fills, the window shrinks toward zero.
window = 0  →  sender pauses entirely until space frees up.
</code>`<code></p>
<p>AeroUDP currently clamps the advertised receive window to at least one packet. An advertised zero therefore still permits a one-packet effective window; complete zero-window backpressure remains a limitation. The </code>peer_window<code> used below is this effective window.</p>
<p>The window "slides" forward as ACKs confirm old data and new data is sent, hence <em>sliding window</em>.</p>
<h3>Out-of-Order Buffering</h3>
<p>Because UDP can reorder packets, the receiver may get packet 7 before packet 5. AeroUDP <strong>buffers out-of-order packets</strong> until the gap fills, then delivers everything to the application in strict order. The application never sees the reordering.</p>
<p>---</p>
<h2>Congestion Control: Don't Overwhelm the Network</h2>
<p>Flow control protects the <em>receiver</em>. <strong>Congestion control</strong> protects the <em>network itself</em>, the shared routers and links between the two endpoints. Without it, many senders can collectively cause <strong>congestion collapse</strong>, where the network is so overloaded that almost nothing gets through.</p>
<p>AeroUDP implements a <strong>NewReno-style</strong> controller, the classic TCP congestion algorithm. Its central variable is the <strong>congestion window (cwnd)</strong>: the sender's own estimate of how much it may safely have in flight. The true limit is:</p>
</code>`<code>
in_flight <= min(cwnd, peer_window)
</code>`<code>
<p>That is: respect both the network (cwnd) <em>and</em> the receiver (peer_window), whichever is smaller.</p>
<h3>Phase 1: Slow Start</h3>
<strong>How it works:</strong>
<ul><li>Start with a small </code>cwnd<code> (default: 10 packets).</li>
<li>For every ACK that advances the cumulative ACK, grow </code>cwnd<code> by the number of segments acknowledged.</li>
<li>This <strong>doubles cwnd roughly every RTT</strong>, exponential growth.</li>
<li>Exit when </code>cwnd >= ssthresh<code> (the slow-start threshold).</li>
</ul>
<strong>Why "slow"?</strong> It starts small (slow) even though it grows fast. The name reflects the conservative starting point, not the growth rate.
</code>`<code>
cwnd: 10 → 20 → 40 → 80 ...   (exponential, per RTT)
</code>`<code>
<h3>Phase 2: Congestion Avoidance</h3>
<strong>How it works:</strong>
<ul><li>Once </code>cwnd >= ssthresh<code>, switch to cautious linear growth.</li>
<li></code>cwnd<code> grows by roughly <strong>1 packet per RTT</strong> (additive increase).</li>
</ul>
</code>`<code>
cwnd: 64 → 65 → 66 → 67 ...   (linear, per RTT)
</code>`<code>
<p>This is the <strong>AIMD</strong> principle, Additive Increase, Multiplicative Decrease, probing gently for more bandwidth while staying ready to back off hard.</p>
<h3>Phase 3: Reacting to Loss</h3>
<p>Loss is the signal that the network is congested. AeroUDP distinguishes two kinds of loss, and reacts to them very differently.</p>
<h4>Fast Retransmit / Fast Recovery (mild loss)</h4>
<strong>Trigger:</strong> three <strong>duplicate ACKs</strong>. When the receiver gets out-of-order packets, it keeps re-sending the same </code>ack_num<code>. Three duplicates strongly suggest a <em>single</em> packet was lost while later ones arrived.
<strong>Reaction:</strong>
<ul><li>Immediately retransmit the missing segment, do <strong>not</strong> wait for the RTO.</li>
<li></code>ssthresh = max(cwnd / 2, 2)<code></li>
<li></code>cwnd = ssthresh<code> (halve, don't reset)</li>
<li>Enter <strong>fast recovery</strong>; exit when a new cumulative ACK passes the recovery point.</li>
</ul>
This is "multiplicative decrease", a measured halving because the network is delivering <em>some</em> packets, so it isn't badly congested.
<h4>Timeout (severe loss)</h4>
<strong>Trigger:</strong> the RTO fires, no ACKs at all.
<strong>Reaction (much harsher):</strong>
<ul><li></code>ssthresh = max(cwnd / 2, 2)<code></li>
<li></code>cwnd = 1<code>, collapse the window</li>
<li>Return to <strong>slow start</strong></li>
</ul>
A full timeout means the network may be severely congested or the path broken, so AeroUDP starts over from near-zero.
<h3>The Sawtooth</h3>
<p>Put together, these phases produce TCP's characteristic <strong>sawtooth</strong> pattern, cwnd climbs, hits loss, halves, climbs again, continuously probing for the maximum safe rate:</p>
</code>`<code>
cwnd
 |        /|      /|      /|
 |       / |     / |     / |
 |      /  |    /  |    /  |
 |     /   |   /   |   /
 |    /    | _/    | _/
 |___/_____|/______|/________ time
      (loss)  (loss)  (loss)
</code>`<code>
<p>---</p>
<h2>Connection Lifecycle</h2>
<p>A connection-oriented protocol has a well-defined birth, life, and death, modeled as a <strong>state machine</strong>. AeroUDP's states mirror TCP's.</p>
<h3>The Three-Way Handshake</h3>
<p>Before data flows, both sides must agree they are connected and synchronize sequence numbers.</p>
</code>`<code>
   Client                          Server
     |                                |
     |  ---- SYN(seq=ISN_a) ------->  |   LISTEN → SYN_RECEIVED
     |  <- SYN_ACK(seq=ISN_b,         |
     |         ack=ISN_a+1) --------  |
     |  ---- ACK(ack=ISN_b+1) ----->  |
     v                                v
  ESTABLISHED                    ESTABLISHED
</code>`<code>
<strong>Why three messages?</strong> Each side must prove it can both <em>send</em> and <em>receive</em>. The SYN proves the client can send; the SYN_ACK proves the server received and can send back; the final ACK proves the client received the server's message. Only then is two-way communication confirmed.
<p>The handshake itself uses the same retransmission-with-backoff machinery, capped by </code>handshake_timeout<code> (5 s) and </code>max_retries<code>.</p>
<h3>Graceful Close (FIN Exchange)</h3>
<p>A connection is a two-way street, so each direction is closed independently ("half-close"):</p>
</code>`<code>
   Initiator                       Peer
     |  ------- FIN --------->      |   → CLOSE_WAIT
   FIN_WAIT_1                       |
     |  <----- FIN_ACK ------       |   (peer may keep sending)
   FIN_WAIT_2                       |
     |  <------- FIN --------       |   LAST_ACK
   TIME_WAIT                        |
     |  ------- ACK -------->       |   → CLOSED
     |
  (wait 2 s to absorb stragglers)
     v
   CLOSED
</code>`<code>
<strong>Why TIME_WAIT?</strong> After the last ACK, the initiator waits (</code>close_wait<code>, default 2 s) before fully closing. This absorbs any delayed retransmissions of the peer's FIN so they don't leak into a future connection reusing the same ports.
<h3>Abort (RST)</h3>
<p>If something goes catastrophically wrong, either side sends a <strong>RST</strong> (reset). The receiver immediately transitions to CLOSED and surfaces a </code>PeerReset<code> event, no graceful exchange, just an abrupt teardown.</p>
<h3>Keepalive (PING / PONG)</h3>
<p>To detect a peer that has silently vanished (crash, cable pull), AeroUDP sends <strong>keepalive probes</strong>:</p>
<ul><li>If nothing is received for </code>keepalive_interval<code> (15 s), send a </code>PING<code>.</li>
<li>The peer replies with </code>PONG<code>.</li>
<li>After </code>keepalive_probes<code> (3) unanswered probes, declare the peer dead and close.</li>
</ul>
---
<h2>Observability: Seeing Inside the Protocol</h2>
<p>A protocol you cannot measure is a protocol you cannot debug or tune. AeroUDP exposes a </code>ConnectionMetrics<code> snapshot backed by atomic counters:</p>
<strong>Traffic:</strong>
<ul><li>Packets and bytes sent/received in both directions</li>
</ul>
<strong>Reliability health:</strong>
<ul><li>Retransmissions, fast retransmissions</li>
<li>Duplicate ACKs, duplicate packets, out-of-order packets</li>
<li>Checksum failures, timeouts</li>
<li>Derived <strong>loss rate</strong></li>
</ul>
<strong>Timing and control state:</strong>
<ul><li>Mean RTT, smoothed RTT (SRTT), RTTVAR, current RTO</li>
<li>Current cwnd, ssthresh, in-flight count</li>
</ul>
It also emits structured </code>tracing<code> events (</code>aeroudp::state<code>, </code>aeroudp::engine<code>, </code>aeroudp::handshake<code>, and more), so you can watch the state machine and congestion controller make decisions in real time.
<h3>Testing Against a Hostile Network</h3>
<p>AeroUDP includes a seeded network simulator with configurable packet loss, duplication, latency, and jitter. From a repository checkout with Rust installed, run the analyzer proxy:</p>
</code>`<code>
cargo run --release -p aeroudp-cli --bin aeroudp-analyzer -- proxy \
    --listen 127.0.0.1:9500 \
    --upstream 127.0.0.1:9000 \
    --loss 0.05 --reorder 0.03 \
    --min-latency-ms 20 --max-latency-ms 60 --jitter-ms 10
</code>`<code>
<p>Run the server on port 9000 and point the client at port 9500. These flags configure fault inputs; verify delivered data and inspect retransmission, RTT, and congestion-window metrics to evaluate a run. The current proxy forwards packets serially per direction, so its reordering flag adds delay without demonstrating reordered delivery.</p>
<p>---</p>
<h2>Putting It All Together</h2>
<p>A single </code>send()<code> call in AeroUDP quietly exercises every concept in this article:</p>
<ul><li>The payload is wrapped in a <strong>28-byte header</strong> with a sequence number and CRC32 <strong>checksum</strong>.</li>
<li>The sender checks <strong>flow control</strong> (peer window) and <strong>congestion control</strong> (cwnd), it only transmits if </code>in_flight < min(cwnd, peer_window)`.</li>
<li>The packet is stored in the <strong>retransmission queue</strong> and sent over UDP.</li>
<li>If it arrives cleanly, the receiver <strong>buffers or delivers in order</strong> and returns a <strong>cumulative ACK</strong>, advancing its <strong>window</strong>.</li>
<li>The ACK confirms the packet, updates the <strong>RTT/RTO estimate</strong>, and grows <strong>cwnd</strong> (slow start or congestion avoidance).</li>
<li>If it is lost, either <strong>three duplicate ACKs</strong> (fast retransmit) or an <strong>RTO timeout</strong> triggers retransmission and shrinks cwnd.</li>
</ul>
None of this is visible to the application, which simply sees a reliable, ordered byte stream, exactly the illusion a transport protocol exists to provide.
<p>---</p>
<h2>Further Reading</h2>
<ul><li><strong>Standards:</strong></li>
</ul>  - <a href="https://www.rfc-editor.org/rfc/rfc9293">RFC 793 / RFC 9293, TCP</a>
  - <a href="https://www.rfc-editor.org/rfc/rfc6298">RFC 6298, Computing TCP's Retransmission Timer</a>
  - <a href="https://www.rfc-editor.org/rfc/rfc5681">RFC 5681, TCP Congestion Control</a>
  - <a href="https://www.rfc-editor.org/rfc/rfc6582">RFC 6582, The NewReno Modification to TCP's Fast Recovery</a>
  - <a href="https://www.rfc-editor.org/rfc/rfc9000">RFC 9000, QUIC Transport Protocol</a>
<ul><li><strong>Books:</strong></li>
</ul>  - "TCP/IP Illustrated, Volume 1" by W. Richard Stevens
  - "Computer Networking: A Top-Down Approach" by Kurose and Ross
<ul><li><strong>Source Code:</strong></li>
</ul>  - <a href="https://github.com/Mo7ammedd/AeroUDP">AeroUDP on GitHub</a>
  - <a href="https://github.com/Mo7ammedd/AeroUDP/blob/main/docs/PROTOCOL.md">AeroUDP Protocol Specification</a>
          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> July 1, 2026<br/>
            <strong>Reading Time:</strong> 14 min read<br/>
            <strong>Tags:</strong> networking, transport-protocols, congestion-control, udp, tcp, rust<br/>
            <a href="https://www.modev.me/blog/aeroudp-networking-concepts" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/aeroudp-networking-concepts</link>
      <guid isPermaLink="true">https://www.modev.me/blog/aeroudp-networking-concepts</guid>
      <pubDate>Wed, 01 Jul 2026 10:00:00 GMT</pubDate>
      <lastBuildDate>Sat, 12 Sep 2026 00:00:00 GMT</lastBuildDate>
      <category>networking</category>
      <category>transport-protocols</category>
      <category>congestion-control</category>
      <category>udp</category>
      <category>tcp</category>
      <category>rust</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/aeroudp-networking-concepts" type="image/png" length="0"/>
    </item>

    <item>
      <title><![CDATA[Pagination Strategies: OFFSET vs Cursor Pagination]]></title>
      <description><![CDATA[OFFSET and cursor based pagination strategies: covering database internals, B-tree traversal, performance characteristics, data consistency problems.]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/pagination-strategies-offset-vs-cursor" alt="Pagination Strategies: OFFSET vs Cursor Pagination" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>Pagination Strategies: OFFSET vs Cursor Pagination</h1>
<p>Pagination is one of the most deceptively simple problems in backend engineering. Every application that lists data needs it. Yet the choice between <strong>offset-based</strong> and <strong>cursor-based</strong> pagination carries enormous implications for query performance, data consistency, scalability, and API design: implications that only become visible under production load.</p>
<p>This post tears into both strategies at the database internals level. We'll look at how each approach interacts with B-tree indexes, what query plans they produce, where their O-complexity comes from, and how to implement each one correctly.</p>
<Callout type="success" title="Key takeaway">
<p>Use OFFSET when readers need to jump to a page in a small or stable dataset. For deep, sequential browsing, use a cursor with a unique sort order and a matching index.</p>
</Callout>
<h2>What Is Pagination, Really?</h2>
<p>Before diving into strategies, let's be precise. Pagination is the technique of breaking a large result set into smaller, sequential chunks called <strong>pages</strong>. The database still holds the full dataset; the client receives one slice at a time.</p>
<p>The core challenge: how do you tell the database <em>where</em> to start returning rows for any given page?</p>
<p>That single question leads to two fundamentally different answers.</p>
<h2>Strategy 1: OFFSET Pagination</h2>
<h3>The Mechanics</h3>
<p>OFFSET pagination works by telling the database: "skip the first N rows and give me the next M."</p>
<p>``<code>sql title="offset-pagination.sql" {5,11,17} showLineNumbers
-- Page 1: skip 0, take 10
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;</p>
<p>-- Page 2: skip 10, take 10
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 10;</p>
<p>-- Page 51: skip 500, take 10
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 500;
</code>`<code></p>
<p>Page number translates directly to </code>OFFSET = (page - 1) * pageSize<code>. Clean. Predictable.</p>
<Cover
  src="/blog/pagination-strategies/offset-pagination-mechanics.webp"
  alt="OFFSET pagination mechanics diagram"
  caption="OFFSET forces the database to count and skip N rows from the beginning on every query"
/>
<h3>What the Database Actually Does</h3>
<p>This is where things get uncomfortable. The SQL standard and every major RDBMS implement OFFSET by <strong>materialising and discarding</strong> the skipped rows. There is no magical shortcut.</p>
<p>When PostgreSQL processes </code>LIMIT 10 OFFSET 500<code>, it:</p>
<ul><li>Evaluates the full </code>WHERE<code> clause to find candidate rows</li>
<li>Sorts those rows by </code>ORDER BY created_at DESC<code></li>
<li>Reads rows 1 through 510 in order</li>
<li><strong>Discards rows 1 through 500</strong></li>
<li>Returns rows 501 through 510</li>
</ul>
Step 4 is pure waste. You are paying the full I/O and CPU cost for rows you immediately throw away.
<p>Let's look at an actual query plan on a table with 10M rows:</p>
<pre><code class="language-sql">EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 500000;
</code></pre>
</code>`<code>
Limit  (cost=89432.17..89432.20 rows=10 width=52)
       (actual time=4821.33..4821.34 rows=10 loops=1)
  Buffers: shared hit=3241 read=52891
  ->  Index Scan Backward using posts_created_at_idx on posts
      (cost=0.56..889234.12 rows=10000000 width=52)
      (actual time=0.04..4718.22 rows=500010 loops=1)
      Buffers: shared hit=3241 read=52891
Planning Time: 0.18 ms
Execution Time: 4821.41 ms
</code>`<code>
<p>The planner uses the index, but it still reads <strong>500,010 rows</strong> to return 10. The index helps with ordering, but offset forces row-by-row counting all the way down to position 500,000.</p>
<h3>The Performance Cliff</h3>
<p>The cost of OFFSET pagination is <strong>O(offset)</strong>: linear in the number of rows to skip. The further you paginate, the slower each query gets.</p>
<Cover
  src="/blog/pagination-strategies/offset-vs-cursor-performance.webp"
  alt="Performance comparison chart: OFFSET degrades linearly while cursor stays flat"
  caption="OFFSET query time grows linearly with page number. Cursor pagination maintains constant O(log n) time regardless of depth."
/>
<p>At page 1 you're reading 10 rows. At page 1,000 (with page size 10), you're reading 10,000 rows to return 10. At page 10,000, you're reading 100,000 rows to return 10. The database does proportionally more work for every deeper page, even though the client receives the same 10 rows each time.</p>
<p>This "performance cliff" is often invisible in development (small datasets) and during early production (few users reaching deep pages). It becomes catastrophic as data grows and users or crawlers navigate deep into result sets.</p>
<h3>The Data Consistency Problem</h3>
<p>OFFSET pagination has a correctness problem that no amount of indexing can fix: <strong>the dataset can shift between page requests</strong>.</p>
<p>If a row is inserted or deleted between two page fetches, the offset arithmetic breaks. Rows get duplicated or silently skipped.</p>
<Cover
  src="/blog/pagination-strategies/offset-phantom-records.webp"
  alt="Diagram showing how OFFSET pagination produces duplicate or missing records during concurrent writes"
  caption="Concurrent inserts and deletes corrupt OFFSET pagination: rows are silently skipped or shown twice"
/>
<strong>Scenario A: Insert causes duplication:</strong> User is on page 1 (</code>OFFSET 0<code>, rows 1-10). A new highest-priority row is inserted at position 1, shifting everything down. User fetches page 2 (</code>OFFSET 10<code>). The database now returns what was originally row 10 (seen on page 1) plus rows 11-19. Row 10 appears twice; row 20 is never shown.
<strong>Scenario B: Delete causes skipping:</strong> User is on page 1, sees rows 1-10. Row 3 is deleted. User fetches page 2 (</code>OFFSET 10<code>). The database skips 10 rows from the new dataset (which only has 9 rows where positions 1-9 used to be 1-10). Row 11 from the original set is now skipped entirely.
<p>In a high-write environment (social feed, live auction, notification list), this is not an edge case: it happens constantly.</p>
<p>OFFSET pagination is <strong>not safe for concurrent write workloads</strong>. Any application where data is inserted, updated, or deleted while users paginate will produce inconsistent results. This includes most real-world applications.</p>
<h3>When OFFSET Is Acceptable</h3>
<p>Despite its problems, OFFSET pagination has legitimate use cases:</p>
<ul><li><strong>Administrative UIs with small datasets</strong>: when your table has < 100K rows and writes are infrequent, the performance hit is negligible</li>
<li><strong>Reporting queries on static snapshots</strong>: if you're paginating a result set that won't change (e.g., a point-in-time export), offset is safe</li>
<li><strong>"Jump to page N" UI requirement</strong>: cursor pagination cannot support arbitrary page jumps; offset can</li>
<li><strong>Simple prototypes</strong>: getting a product working quickly where pagination performance is not yet a concern</li>
</ul>
<h2>Strategy 2: Cursor Pagination</h2>
<h3>The Core Idea</h3>
<p>Instead of asking "skip N rows", cursor pagination asks: "give me rows <em>after</em> this specific position." The position is encoded in a <strong>cursor</strong>: an opaque token the client receives with each response and sends back on the next request.</p>
</code>`<code>sql title="cursor-pagination.sql" {10-11} showLineNumbers
-- First page (no cursor)
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 10;
<p>-- Subsequent pages (cursor = last row from previous page)
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ('2026-03-10 14:23:00', 5892)
ORDER BY created_at DESC, id DESC
LIMIT 10;
</code>`<code></p>
<p>The </code>WHERE<code> clause anchors the query to a specific position in the index. The database doesn't skip anything: it seeks directly to the cursor position and reads forward.</p>
<Cover
  src="/blog/pagination-strategies/cursor-pagination-mechanics.webp"
  alt="Cursor pagination mechanics showing direct B-tree index seek"
  caption="Cursor pagination uses a WHERE clause to seek directly to the cursor position in the B-tree index: no rows are skipped or wasted"
/>
<h3>Try the Pagination Scan Model</h3>
<p>Change the page depth and compare the index entries each strategy visits for one request. This model assumes the cursor for that page is already available from a previous response.</p>
<PaginationDemo />
<h3>B-Tree Internals: Why Cursors Are Fast</h3>
<p>To understand why cursor pagination outperforms OFFSET at scale, you need to understand how B-tree indexes work.</p>
<Cover
  src="/blog/pagination-strategies/btree-traversal-comparison.webp"
  alt="B-Tree traversal comparison between OFFSET and Cursor pagination"
  caption="OFFSET must traverse all leaf nodes up to the skip count. Cursor pagination performs a direct seek using binary search: O(log n) regardless of position."
/>
<p>A B-tree index is a balanced tree structure. The leaf nodes are doubly-linked and contain the actual sorted key values. Every lookup starts at the root and descends through internal nodes using binary comparisons until reaching the correct leaf.</p>
<strong>OFFSET traversal:</strong>
<ul><li>Descend from root to leftmost leaf (O(log n))</li>
<li><strong>Scan forward through leaf nodes counting rows until the offset is reached</strong> (O(offset))</li>
<li>Return the next LIMIT rows</li>
</ul>
Step 2 dominates. For </code>OFFSET 500000<code>, you're touching ~500,000 leaf entries sequentially. Each leaf page holds ~100-200 entries, so that's ~2,500-5,000 buffer reads just to reach the starting position.
<strong>Cursor traversal:</strong>
<Steps>
<Step title="Seek to the cursor">
<p>Descend from the root to the correct leaf using the cursor value: <strong>O(log n)</strong>.</p>
</Step>
<Step title="Read the next page">
<p>Read the next </code>LIMIT<code> rows from that position: <strong>O(limit)</strong>.</p>
</Step>
<Step title="Continue from the last row">
<p>Return the page and use its final row as the cursor for the next request.</p>
</Step>
</Steps>
<p>For a table with 10M rows, </code>log₂(10,000,000) ≈ 23<code>. Regardless of which "page" you're on: page 1 or page 1,000,000: cursor pagination only traverses ~23 tree levels to find the starting position. The complexity is <strong>O(log n + limit)</strong>, effectively constant relative to dataset size.</p>
<h3>The Composite Cursor Problem</h3>
<p>Simple cursors based on a single monotonically increasing integer ID work perfectly. Real world queries are messier. What if you're ordering by </code>created_at<code>, which is not unique?</p>
<pre><code class="language-sql">-- WRONG: created_at is not unique: ties cause missed rows
WHERE created_at < '2026-03-10 14:23:00'
ORDER BY created_at DESC
LIMIT 10;
</code></pre>
<Callout type="warning" title="Common mistake: a cursor without a tiebreaker">
<p>If multiple rows share the same </code>created_at<code> timestamp (common with bulk inserts), rows at the boundary can be skipped. Use a <strong>composite cursor</strong> that includes a unique tiebreaker, typically the primary key, in both the filter and the sort order.</p>
</Callout>
<pre><code class="language-sql">-- CORRECT: composite cursor with tiebreaker
WHERE (created_at, id) < ('2026-03-10 14:23:00', 5892)
ORDER BY created_at DESC, id DESC
LIMIT 10;
</code></pre>
<p>This requires a <strong>composite index</strong> on </code>(created_at DESC, id DESC)<code>:</p>
<pre><code class="language-sql">CREATE INDEX idx_posts_cursor ON posts (created_at DESC, id DESC);
</code></pre>
<p>Without this index, the composite WHERE clause cannot be satisfied with an index seek and falls back to a sequential scan.</p>
<p>Always create a composite index that exactly matches your cursor columns and sort direction. PostgreSQL can use an index scan backward if the index is in ascending order and your query is </code>DESC<code>, but an explicit </code>DESC<code> index avoids ambiguity and is often faster in practice.</p>
<h3>Encoding the Cursor</h3>
<p>The cursor value should be <strong>opaque</strong> to clients: they should not parse, construct, or predict it. Expose a base64-encoded JSON token that the server decodes internally.</p>
<Cover
  src="/blog/pagination-strategies/cursor-api-flow.webp"
  alt="API flow diagram showing cursor token encoding, transmission, and decoding"
  caption="The cursor is an opaque base64 token. Clients treat it as a black box: they receive it, store it, and send it back."
/>
<pre><code class="language-typescript">// Encoding a cursor
interface CursorPayload {
  created_at: string
  id: number
}
<p>function encodeCursor(payload: CursorPayload): string {
  return Buffer.from(JSON.stringify(payload)).toString('base64url')
}</p>
<p>// Decoding a cursor
function decodeCursor(token: string): CursorPayload {
  try {
    return JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'))
  } catch {
    throw new Error('Invalid pagination cursor')
  }
}
</code></pre></p>
<p>Using </code>base64url<code> (URL-safe base64) avoids the need to URL-encode the token when passing it as a query parameter.</p>
<h3>Full Implementation Example</h3>
<p>Here is a production-grade cursor pagination implementation in TypeScript with PostgreSQL:</p>
<pre><code class="language-typescript">
<p>interface Post {
  id: number
  title: string
  created_at: Date
}</p>
<p>interface PaginatedResult<T> {
  data: T[]
  next_cursor: string | null
  has_more: boolean
}</p>
<p>interface CursorPayload {
  created_at: string
  id: number
}</p>
<p>function encodeCursor(payload: CursorPayload): string {
  return Buffer.from(JSON.stringify(payload)).toString('base64url')
}</p>
<p>function decodeCursor(token: string): CursorPayload {
  try {
    const decoded = JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'))
    if (!decoded.created_at || typeof decoded.id !== 'number') {
      throw new Error('Invalid cursor shape')
    }
    return decoded
  } catch {
    throw new Error('Malformed pagination cursor')
  }
}</p>
<p>async function getPosts(
  pool: Pool,
  limit: number = 10,
  cursor?: string
): Promise<PaginatedResult<Post>> {
  // Request one extra row to determine if there is a next page
  const pageSize = limit + 1</p>
<p>let rows: Post[]</p>
<p>if (!cursor) {
    // First page: no cursor condition
    const result = await pool.query<Post>(
      </code>SELECT id, title, created_at
       FROM posts
       ORDER BY created_at DESC, id DESC
       LIMIT $1<code>,
      [pageSize]
    )
    rows = result.rows
  } else {
    const { created_at, id } = decodeCursor(cursor)</p>
<p>// Subsequent pages: use composite cursor
    const result = await pool.query<Post>(
      </code>SELECT id, title, created_at
       FROM posts
       WHERE (created_at, id) < ($1::timestamptz, $2)
       ORDER BY created_at DESC, id DESC
       LIMIT $3<code>,
      [created_at, id, pageSize]
    )
    rows = result.rows
  }</p>
<p>const has_more = rows.length > limit
  const data = has_more ? rows.slice(0, limit) : rows</p>
<p>const last = data[data.length - 1]
  const next_cursor =
    has_more && last
      ? encodeCursor({
          created_at: last.created_at.toISOString(),
          id: last.id,
        })
      : null</p>
<p>return { data, next_cursor, has_more }
}
</code></pre></p>
<p>The "fetch limit + 1" trick is the standard way to determine </code>has_more<code> without running a separate </code>COUNT(*)<code> query.</p>
<h3>Bidirectional Cursor Pagination</h3>
<p>Sometimes you need both "next page" and "previous page" navigation. This requires storing two cursors: one for the first item and one for the last item on the current page.</p>
<pre><code class="language-typescript">interface BidirectionalResult<T> {
  data: T[]
  page_info: {
    start_cursor: string | null
    end_cursor: string | null
    has_next_page: boolean
    has_previous_page: boolean
  }
}
</code></pre>
<p>To paginate backward (</code>before<code> cursor):</p>
<pre><code class="language-sql">-- Get the previous page: rows just before the start cursor
-- Wrap in a subquery to re-sort after reversing
SELECT * FROM (
  SELECT id, title, created_at
  FROM posts
  WHERE (created_at, id) > ($1::timestamptz, $2)   -- reversed comparison
  ORDER BY created_at ASC, id ASC                   -- reversed sort
  LIMIT $3
) sub
ORDER BY created_at DESC, id DESC;                  -- re-sort for display
</code></pre>
<p>This pattern is used by the <strong>Relay cursor connection spec</strong>: the standard pagination API contract used by most GraphQL APIs.</p>
<h2>Database Specific Behaviour</h2>
<h3>PostgreSQL</h3>
<p>PostgreSQL's planner is excellent at recognising cursor patterns. A composite </code>WHERE (a, b) < ($1, $2)<code> is properly decomposed into range conditions the planner can use with a composite index:</p>
<pre><code class="language-sql">-- PostgreSQL rewrites this:
WHERE (created_at, id) < ('2026-03-10', 5892)
<p>-- Into the equivalent:
WHERE created_at < '2026-03-10'
   OR (created_at = '2026-03-10' AND id < 5892)
</code></pre></p>
<p>This rewrite uses the index correctly. Verify with </code>EXPLAIN<code>:</p>
<pre><code class="language-sql">EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ('2026-03-10 14:23:00'::timestamptz, 5892)
ORDER BY created_at DESC, id DESC
LIMIT 10;
</code></pre>
</code>`<code>
Limit  (cost=0.56..1.23 rows=10 width=52)
       (actual time=0.041..0.052 rows=10 loops=1)
  Buffers: shared hit=5
  ->  Index Scan Backward using idx_posts_cursor on posts
      (cost=0.56..671234.44 rows=10000000 width=52)
      (actual time=0.038..0.048 rows=10 loops=1)
      Index Cond: (ROW(created_at, id) < ROW('2026-03-10 14:23:00+00'::timestamptz, 5892))
      Buffers: shared hit=5
Planning Time: 0.22 ms
Execution Time: 0.07 ms
</code>`<code>
<p>Only <strong>5 buffer hits</strong> to return 10 rows from a 10M-row table. Compare that to 52,891 buffer hits for the OFFSET 500,000 query above.</p>
<h3>MySQL / MariaDB</h3>
<p>MySQL handles row value expressions less elegantly. The </code>(a, b) < ($1, $2)<code> syntax is supported but the optimizer does not always use it efficiently. The explicit expansion is safer:</p>
<pre><code class="language-sql">-- Explicit expansion: more reliable in MySQL
SELECT id, title, created_at
FROM posts
WHERE created_at < '2026-03-10 14:23:00'
   OR (created_at = '2026-03-10 14:23:00' AND id < 5892)
ORDER BY created_at DESC, id DESC
LIMIT 10;
</code></pre>
<p>Ensure the composite index exists: </code>CREATE INDEX idx_posts_cursor ON posts (created_at DESC, id DESC);<code></p>
<h3>SQL Server</h3>
<p>SQL Server supports row value expressions with </code>(a, b) < ($1, $2)<code> syntax and uses it correctly with covering indexes. Additionally, SQL Server's </code>FETCH NEXT ... ROWS ONLY<code> syntax (standard OFFSET-FETCH) has the same performance characteristics as </code>LIMIT ... OFFSET<code>.</p>
<h2>Concurrency and Snapshot Isolation</h2>
<p>One underappreciated advantage of cursor pagination: it <strong>naturally respects snapshot isolation</strong> in databases that support it (PostgreSQL, MySQL InnoDB with REPEATABLE READ, SQL Server with snapshot isolation).</p>
<p>When a cursor-paginated query runs, the WHERE condition </code>(created_at, id) < (cursor_value)<code> is deterministic and set-based. Rows that have been inserted or deleted since the first page was fetched affect only the pages where they would logically appear. Rows the user has already seen remain unchanged. Rows ahead of the cursor are fetched as they exist at query time.</p>
<p>In contrast, OFFSET shifts the entire logical position of every row, so any change anywhere in the table can corrupt results for any active pagination session.</p>
<p>Cursor pagination provides <strong>stable pagination windows</strong> in the presence of concurrent writes. New rows inserted after the cursor position appear in subsequent pages; rows before the cursor are unaffected. This is particularly valuable for real-time feeds (Twitter/X, Instagram, notification inboxes).</p>
<h2>Compound Ordering and Edge Cases</h2>
<h3>Non-Sequential Cursors</h3>
<p>Cursor pagination assumes a <strong>consistent, stable sort order</strong>. If you allow users to change sort direction mid-session (e.g., flip from "newest first" to "oldest first"), the cursor from the previous sort is invalid for the new sort. Each sort combination needs its own cursor scheme.</p>
<pre><code class="language-typescript">type SortField = 'created_at' | 'likes' | 'comments'
type SortDirection = 'asc' | 'desc'
<p>interface CursorPayload {
  sort_field: SortField
  sort_direction: SortDirection
  sort_value: string | number
  id: number             // tiebreaker always included
}
</code></pre></p>
<p>Validate that the cursor's </code>sort_field<code> and </code>sort_direction<code> match the current request. Reject stale cursors with a </code>400 Bad Request<code>.</p>
<h3>NULL Values in Cursor Columns</h3>
<p>If your cursor column can contain NULLs, comparison semantics break down. SQL's three-valued logic means </code>NULL < 5<code> is </code>NULL<code> (neither true nor false), not </code>true<code>. You have two options:</p>
<ul><li><strong>Exclude NULLs</strong> from the result set if possible (</code>WHERE sort_column IS NOT NULL<code>)</li>
<li><strong>Use COALESCE</strong> to give NULLs a sentinel value that sorts consistently</li>
</ul>
<pre><code class="language-sql">-- Option 2: treat NULL as the minimum value (sorts last in DESC)
WHERE (COALESCE(score, -1), id) < ($1, $2)
ORDER BY COALESCE(score, -1) DESC, id DESC
</code></pre>
<h3>Filtered Cursors</h3>
<p>When pagination is combined with filtering (e.g., </code>WHERE status = 'published'<code>), the cursor must respect the filter:</p>
<pre><code class="language-sql">SELECT id, title, created_at
FROM posts
WHERE status = 'published'
  AND (created_at, id) < ($1::timestamptz, $2)
ORDER BY created_at DESC, id DESC
LIMIT 10;
</code></pre>
<p>The index should be a <strong>partial index</strong> or include the filter column:</p>
<pre><code class="language-sql">-- Partial index for published posts only
CREATE INDEX idx_posts_published_cursor
ON posts (created_at DESC, id DESC)
WHERE status = 'published';
</code></pre>
<p>This dramatically reduces index size and improves seek performance when the filtered subset is a small fraction of the total table.</p>
<h2>GraphQL Relay Cursor Connections</h2>
<p>The most standardised cursor pagination API is the <strong>Relay Connection Specification</strong>, used as the de-facto standard for GraphQL APIs.</p>
<pre><code class="language-graphql">query GetPosts($first: Int, $after: String) {
  posts(first: $first, after: $after) {
    edges {
      cursor
      node {
        id
        title
        createdAt
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
  }
}
</code></pre>
<p>The Relay spec mandates:
<ul><li></code>first<code> / </code>after<code> for forward pagination (equivalent to our </code>limit<code> / </code>cursor<code>)</li>
<li></code>last<code> / </code>before<code> for backward pagination</li>
<li>Each </code>edge<code> contains the item (</code>node<code>) and its </code>cursor<code></li>
<li></code>pageInfo<code> exposes cursors for the current page boundary</li>
</ul>
<pre><code class="language-typescript">// Relay-compatible resolver (simplified)
async function postsConnection(
  args: { first?: number; after?: string; last?: number; before?: string }
) {
  if (args.first !== undefined) {
    return forwardPaginate(args.first, args.after)
  }
  if (args.last !== undefined) {
    return backwardPaginate(args.last, args.before)
  }
  throw new Error('Must provide first or last')
}
</code></pre></p>
<h2>REST API Design for Cursor Pagination</h2>
<p>A clean REST cursor pagination contract:</p>
<pre><code class="language-http">GET /api/posts?limit=10
</code></pre>
<pre><code class="language-json">{
  "data": [
    { "id": 2051, "title": "Latest post", "created_at": "2026-03-13T09:00:00Z" },
    ...
  ],
  "pagination": {
    "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMy0xMFQxNDoyMzowMFoiLCJpZCI6NTg5Mn0",
    "has_more": true,
    "limit": 10
  }
}
</code></pre>
<pre><code class="language-http">GET /api/posts?limit=10&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMy0xMFQxNDoyMzowMFoiLCJpZCI6NTg5Mn0
</code></pre>
<p>Design rules:
<ul><li></code>cursor<code> is always optional on the first request</li>
<li></code>next_cursor<code> is </code>null<code> when there are no more pages</li>
<li>Never expose raw database values in the cursor (always encode)</li>
<li>Validate cursor structure on decode; return </code>400<code> for invalid cursors</li>
<li>Consider cursor expiry for very large datasets (cursors pointing to deleted rows)</li>
</ul></p>
<h2>Choosing the Right Strategy</h2>
<table>
  <thead>
    <tr>
      <th>Criterion</th>
      <th>OFFSET</th>
      <th>Cursor</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Performance (early pages)</strong></td>
      <td>Fast</td>
      <td>Fast</td>
    </tr>
    <tr>
      <td><strong>Performance (deep pages)</strong></td>
      <td>O(offset) slow</td>
      <td>O(log n) fast</td>
    </tr>
    <tr>
      <td><strong>Correctness under writes</strong></td>
      <td>Inconsistent</td>
      <td>Consistent</td>
    </tr>
    <tr>
      <td><strong>Arbitrary page jump</strong></td>
      <td>Yes</td>
      <td>No</td>
    </tr>
    <tr>
      <td><strong>Total count</strong></td>
      <td>Easy (COUNT(*))</td>
      <td>Expensive / approximate</td>
    </tr>
    <tr>
      <td><strong>Sort flexibility</strong></td>
      <td>Any column, any order</td>
      <td>Must match cursor columns</td>
    </tr>
    <tr>
      <td><strong>API complexity</strong></td>
      <td>Simple</td>
      <td>Moderate</td>
    </tr>
    <tr>
      <td><strong>Infinite scroll / feeds</strong></td>
      <td>Poor</td>
      <td>Excellent</td>
    </tr>
    <tr>
      <td><strong>Export / batch processing</strong></td>
      <td>Acceptable</td>
      <td>Preferred</td>
    </tr>
    <tr>
      <td><strong>Large dataset (&gt; 1M rows)</strong></td>
      <td>Problematic</td>
      <td>Scales indefinitely</td>
    </tr>
  </tbody>
</table>
<p>A common hybrid: use OFFSET for page 1-5 (fast enough, simpler API) and switch to cursor-based for deeper pages. This requires either tracking a cursor checkpoint at page 5, or accepting that offset inconsistency applies only for early pages.</p>
<h3>Decision Framework</h3>
<strong>Use OFFSET when:</strong>
<ul><li>Dataset is small and stable (< 100K rows, low write rate)</li>
<li>UI requires "jump to page N" or "total pages" display</li>
<li>Building a quick prototype or admin tool</li>
<li>The query is a one-off report on an immutable snapshot</li>
</ul>
<strong>Use Cursor when:</strong>
<ul><li>Dataset is large (hundreds of thousands to billions of rows)</li>
<li>Data is written concurrently while users paginate</li>
<li>You're building an infinite scroll, feed, or API consumed programmatically</li>
<li>You need predictable latency regardless of pagination depth</li>
<li>You're exposing a public API where clients may paginate arbitrarily deep</li>
</ul>
<h2>Keyset Pagination: The Generalisation</h2>
<p>Cursor pagination is a specific application of a broader technique called <strong>keyset pagination</strong>. The key insight is: instead of specifying a position by count from the beginning, specify it by value in the sort key.</p>
<p>Any query of the form:</p>
<pre><code class="language-sql">WHERE sort_key > :last_seen_value
ORDER BY sort_key ASC
LIMIT :n
</code></pre>
<p>is keyset pagination. The "cursor" is just the encoded </code>last_seen_value<code>. This works for any sortable, indexed column: integer IDs, timestamps, UUIDs (with ordered UUID strategies like UUIDv7), composite keys, and more.</p>
<strong>UUIDv7 as a cursor column</strong> is an increasingly popular pattern: UUIDv7 is time-ordered (like a timestamp) but globally unique (no tiebreaker needed), making it ideal as a single-column cursor in distributed systems.
<pre><code class="language-sql">-- UUIDv7 cursor: no tiebreaker needed because UUIDs are globally unique
SELECT id, title, created_at
FROM posts
WHERE id > $1::uuid
ORDER BY id ASC
LIMIT 10;
</code></pre>
<h2>Verified Examples</h2>
<p>Checked on <strong>September 16, 2026</strong> with <strong>PostgreSQL 18.6</strong>. The SQL check used 103 rows with tied timestamps and 10-row pages. It compared OFFSET and composite-cursor results across all 11 pages, including the final partial page and an exhausted cursor, and confirmed that a timestamp-only cursor can skip tied rows.</p>
<p>The reproducible check is </code>scripts/verify-blog/pagination.sql` in the portfolio repository. It verifies query results and page boundaries; the interactive model above illustrates index entry counts under its stated assumptions.</p>
          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> March 13, 2026<br/>
            <strong>Reading Time:</strong> 19 min read<br/>
            <strong>Tags:</strong> database, pagination, performance, sql, backend, postgresql<br/>
            <a href="https://www.modev.me/blog/pagination-strategies-offset-vs-cursor" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/pagination-strategies-offset-vs-cursor</link>
      <guid isPermaLink="true">https://www.modev.me/blog/pagination-strategies-offset-vs-cursor</guid>
      <pubDate>Fri, 13 Mar 2026 10:00:00 GMT</pubDate>
      <lastBuildDate>Wed, 16 Sep 2026 00:00:00 GMT</lastBuildDate>
      <category>database</category>
      <category>pagination</category>
      <category>performance</category>
      <category>sql</category>
      <category>backend</category>
      <category>postgresql</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/pagination-strategies-offset-vs-cursor" type="image/png" length="0"/>
    </item>

    <item>
      <title><![CDATA[Nginx Architecture, Configuration, and Production Patterns]]></title>
      <description><![CDATA[Nginx architecture and production configuration.]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/nginx-deep-dive-architecture-configuration-production-patterns" alt="Nginx Architecture, Configuration, and Production Patterns" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>Nginx: From Request Flow to Production Deployment</h1>
<h2>Introduction</h2>
<p>Nginx is one of the most widely used web servers and reverse proxies in production systems. It powers high-traffic websites, APIs, microservices platforms, and edge gateways worldwide. Companies like Netflix, Dropbox, and WordPress.com rely on Nginx to handle millions of concurrent connections.</p>
<p>Unlike traditional process-based servers (Apache with prefork MPM), Nginx is built around an <strong>event-driven, non-blocking architecture</strong>. This fundamental design difference allows it to handle tens of thousands of concurrent connections with predictable resource usage and minimal memory footprint.</p>
<p>Nginx is commonly deployed for:</p>
<ul><li><strong>Serving static content</strong> - HTML, CSS, JavaScript, images with sendfile() optimization</li>
<li><strong>Reverse proxy</strong> - Abstracting backend services behind a unified interface</li>
<li><strong>Load balancing</strong> - Distributing traffic across multiple backend servers</li>
<li><strong>TLS termination</strong> - Offloading SSL/TLS encryption from application servers</li>
<li><strong>API gateways</strong> - Request routing, authentication, rate limiting</li>
<li><strong>Zero-downtime deployments</strong> - Graceful configuration reloads without dropping connections</li>
<li><strong>Security filtering</strong> - DDoS protection, request validation, WAF integration</li>
</ul>
To use Nginx effectively in production, you must understand <strong>how requests flow through its processing pipeline</strong>, <strong>how configuration directives are evaluated</strong>, and <strong>how architectural decisions affect performance characteristics</strong>.
<p>---</p>
<h2>Nginx Architecture: The Master-Worker Model</h2>
<p>!<a href="/blog/nginx/nginx-architecture-overview.webp">Nginx Architecture Overview</a></p>
<p>Nginx uses a <strong>master-worker process model</strong> that differs fundamentally from traditional web servers like Apache's prefork MPM.</p>
<h3>Master Process Responsibilities</h3>
<p>The master process runs as root (or with CAP_NET_BIND_SERVICE on Linux) and performs privileged operations:</p>
<ul><li><strong>Configuration management</strong> - Reads, parses, and validates nginx.conf</li>
<li><strong>Port binding</strong> - Binds to privileged ports (80, 443) before dropping privileges</li>
<li><strong>Worker lifecycle</strong> - Spawns, monitors, and restarts worker processes</li>
<li><strong>Signal handling</strong> - Processes SIGHUP (reload), SIGTERM (shutdown), SIGUSR1 (log rotation)</li>
<li><strong>Graceful operations</strong> - Coordinates zero-downtime configuration reloads</li>
<li><strong>Binary upgrades</strong> - Enables hot-swapping Nginx binary without downtime</li>
</ul>
The master process does <strong>not</strong> handle client connections. It's purely a supervisor.
<h3>Worker Process Design</h3>
<p>Worker processes run as unprivileged users (typically <code>nginx</code> or <code>www-data</code>) and handle all client traffic:</p>
<pre><code class="language-nginx">worker_processes auto;  # One per CPU core
worker_cpu_affinity auto;  # Pin workers to specific CPUs
</code></pre>
<p>Each worker operates independently with:</p>
<ul><li><strong>Event loop</strong> - Uses epoll (Linux), kqueue (FreeBSD), or event ports (Solaris)</li>
<li><strong>Non-blocking I/O</strong> - All socket operations are async</li>
<li><strong>Connection pooling</strong> - Reuses connections to upstream servers</li>
<li><strong>Memory management</strong> - Per-worker memory pools for efficient allocation</li>
</ul>
<h3>Why This Architecture Matters</h3>
<p>Traditional process-per-connection models (Apache prefork) consume resources linearly:</p>
<ul><li>10,000 connections = 10,000 processes = GBs of memory</li>
<li>Context switching overhead increases with connection count</li>
<li>Resource limits become bottlenecks (file descriptors, memory)</li>
</ul>
Nginx's event-driven model scales differently:
<ul><li>10,000 connections = 4-8 worker processes = hundreds of MBs</li>
<li>No context switching between connections</li>
<li>Predictable memory usage regardless of connection count</li>
</ul>
<strong>Result</strong>: A single Nginx instance can handle 100,000+ concurrent connections on modest hardware.
<p>---</p>
<h2>Event-Driven Architecture Deep Dive</h2>
<p>Nginx workers use an event loop similar to Node.js, but implemented in C for maximum performance.</p>
<h3>The Event Loop</h3>
<p>Each worker runs a continuous loop:</p>
<pre><code class="language-c">while (true) {
    // Wait for events (readable sockets, timers, signals)
    events = epoll_wait(epfd, max_events, timeout);
    
    // Process each event without blocking
    for (event in events) {
        if (event.type == READ) {
            read_request_nonblocking(event.socket);
        } else if (event.type == WRITE) {
            write_response_nonblocking(event.socket);
        } else if (event.type == TIMER) {
            handle_timeout(event.connection);
        }
    }
    
    // Process deferred handlers
    process_posted_events();
}
</code></pre>
<h3>Non-Blocking I/O Implementation</h3>
<p>All socket operations use O_NONBLOCK flag:</p>
<ul><li><code>accept()</code> returns immediately with EAGAIN if no connections pending</li>
<li><code>read()</code> returns partial data or EAGAIN without blocking</li>
<li><code>write()</code> returns bytes written or EAGAIN if socket buffer full</li>
</ul>
If an operation can't complete immediately:
<ul><li>Register interest with epoll/kqueue</li>
<li>Return to event loop</li>
<li>Resume when socket becomes ready</li>
</ul>
This allows a single worker to juggle thousands of connections without threads.
<p>---</p>
<h2>Request Processing Pipeline</h2>
<p>!<a href="/blog/nginx/nginx-request-lifecycle-1.png">Nginx Request Lifecycle</a></p>
<p>Understanding Nginx's request processing phases is crucial for correct configuration.</p>
<h3>Processing Phases (in order)</h3>
<ul><li><strong>POST_READ</strong> - Request headers just read</li>
<li><strong>SERVER_REWRITE</strong> - Server-level rewrites (before location matching)</li>
<li><strong>FIND_CONFIG</strong> - Location block selection</li>
<li><strong>REWRITE</strong> - Location-level rewrites</li>
<li><strong>POST_REWRITE</strong> - Check if URI changed (may restart from FIND_CONFIG)</li>
<li><strong>PREACCESS</strong> - Modules that run before access check (limit_req, limit_conn)</li>
<li><strong>ACCESS</strong> - Access control (allow, deny, auth_basic)</li>
<li><strong>POST_ACCESS</strong> - Check access results</li>
<li><strong>PRECONTENT</strong> - try_files directive runs here</li>
<li><strong>CONTENT</strong> - Generate response (proxy_pass, fastcgi_pass, return, etc.)</li>
<li><strong>LOG</strong> - Write access logs</li>
</ul>
<h3>Phase Execution Rules</h3>
<ul><li>Phases run sequentially, but some can be skipped</li>
<li>Multiple handlers can register for a phase</li>
<li>A handler can short-circuit the pipeline (e.g., <code>return 403</code>)</li>
<li>Location rewrites can restart the pipeline from FIND_CONFIG</li>
</ul>
<h3>Example Request Flow</h3>
<pre><code class="language-nginx">server {
    listen 80;
    server_name api.example.com;
    
    # SERVER_REWRITE phase
    rewrite ^/v1/(.*)$ /api/v1/$1 last;
    
    location /api/ {
        # REWRITE phase
        rewrite ^/api/v1/users$ /api/v1/users/ permanent;
        
        # PREACCESS phase
        limit_req zone=api burst=20;
        
        # ACCESS phase
        allow 10.0.0.0/8;
        deny all;
        
        # CONTENT phase
        proxy_pass http://backend;
    }
}
</code></pre>
<strong>Execution for <code>/v1/users</code></strong>:
<ul><li>SERVER_REWRITE: URI becomes <code>/api/v1/users</code></li>
<li>FIND_CONFIG: Matches <code>/api/</code> location</li>
<li>REWRITE: URI becomes <code>/api/v1/users/</code> (301 redirect sent)</li>
<li>Pipeline stops, response returned</li>
</ul>
---
<h2>Configuration File Structure and Inheritance</h2>
<p>Nginx configuration uses a hierarchical, context-based structure with specific inheritance rules.</p>
<h3>Context Hierarchy</h3>
<pre><code class="language-nginx">main          # Global directives
├── events    # Connection processing settings
└── http      # HTTP server settings
    ├── upstream    # Backend server groups
    ├── server      # Virtual host
    │   └── location    # URI-specific rules
    │       └── if      # Conditional logic (use sparingly)
    └── map     # Variable mappings
</code></pre>
<h3>Directive Context Rules</h3>
<p>Each directive is only valid in specific contexts:</p>
<ul><li><code>worker_processes</code> - main only</li>
<li><code>listen</code> - server only</li>
<li><code>proxy_pass</code> - location, if in location</li>
<li><code>root</code> - http, server, location, if in location</li>
</ul>
Wrong context = configuration error.
<h3>Inheritance Behavior</h3>
<p>Child contexts inherit from parents but can override:</p>
<pre><code class="language-nginx">http {
    client_max_body_size 10m;  # Default for all servers
    
    server {
        # Inherits 10m
        
        location /upload {
            client_max_body_size 100m;  # Override for this location
        }
    }
}
</code></pre>
<strong>Important</strong>: Some directives merge (add_header), others replace (root, proxy_pass).
<p>---</p>
<h2>Location Matching: The Most Critical Concept</h2>
<p>Location matching determines which block handles a request. Getting this wrong causes 90% of Nginx misconfigurations.</p>
<h3>Match Types (in priority order)</h3>
<ul><li><strong>Exact match</strong> <code>= /path</code></li>
<li><strong>Preferential prefix</strong> <code>^~ /path</code></li>
<li><strong>Regex (case-sensitive)</strong> <code>~ pattern</code></li>
<li><strong>Regex (case-insensitive)</strong> <code>~* pattern</code></li>
<li><strong>Longest prefix</strong> <code>/path</code></li>
</ul>
<h3>Matching Algorithm</h3>
<p>``<code>
<ul><li>Check for exact match (=)</li>
</ul>   └─> If found, stop immediately</p>
<ul><li>Store longest prefix match</li>
</ul>   └─> If it's preferential (^~), stop
<ul><li>Test regexes in order of appearance</li>
</ul>   └─> If match found, use it and stop
<ul><li>If no regex matched, use stored prefix</li>
</ul></code>`<code>
<h3>Common Pitfalls</h3>
<strong>Mistake #1: Regex overrides longer prefix</strong>
<pre><code class="language-nginx">location /api/ { proxy_pass http://api; }      # Prefix
location ~ \.php$ { fastcgi_pass php; }        # Regex
</code></pre>
<p>Request </code>/api/test.php<code> hits the PHP location, not /api/!</p>
<strong>Fix: Use preferential prefix</strong>
<pre><code class="language-nginx">location ^~ /api/ { proxy_pass http://api; }   # Stops before regex
location ~ \.php$ { fastcgi_pass php; }
</code></pre>
<strong>Mistake #2: Forgetting trailing slashes</strong>
<pre><code class="language-nginx">location /api { proxy_pass http://backend; }
</code></pre>
<p>Matches </code>/api<code>, </code>/api123<code>, </code>/apitest<code> - probably not what you want.</p>
<strong>Fix: Be specific</strong>
<pre><code class="language-nginx">location = /api { }          # Exact /api only
location /api/ { }           # /api/anything
location ~ ^/api(/|$) { }    # /api or /api/anything
</code></pre>
<p>---</p>
<h2>Location Matching Visual Guide</h2>
<p>!<a href="/blog/nginx/nginx-location-matching-2.webp">Location Matching Priority</a></p>
<h3>Real-World Examples</h3>
<pre><code class="language-nginx">server {
    # Health check - fastest possible response
    location = /health {
        access_log off;
        return 200 "OK\n";
    }
    
    # API routes - must not hit static file handlers
    location ^~ /api/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
    
    # Static assets with caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # PHP files
    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    
    # Default fallback
    location / {
        try_files $uri $uri/ =404;
    }
}
</code></pre>
<strong>Request routing</strong>:
<ul><li></code>/health<code> → Exact match, returns 200 immediately</li>
<li></code>/api/users<code> → Preferential prefix, proxies to backend</li>
<li></code>/logo.png<code> → Regex match, serves with cache headers</li>
<li></code>/index.php<code> → Regex match, passes to PHP-FPM</li>
<li></code>/about<code> → Prefix match, tries file then 404</li>
</ul>
---
<h2>Reverse Proxy Configuration</h2>
<p>!<a href="/blog/nginx/nginx-reverse-proxy.png">Nginx Reverse Proxy Architecture</a></p>
<p>Nginx excels as a reverse proxy, sitting between clients and application servers.</p>
<h3>Basic Proxy Configuration</h3>
<pre><code class="language-nginx">location / {
    proxy_pass http://backend;
}
</code></pre>
<p>This works, but is suboptimal for production.</p>
<h3>Production-Grade Proxy Config</h3>
<pre><code class="language-nginx">location / {
    # Backend address
    proxy_pass http://backend;
    
    # HTTP version (1.1 required for keepalive)
    proxy_http_version 1.1;
    
    # Essential headers
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # WebSocket support
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    
    # Timeouts
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
    
    # Buffering
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    
    # Connection reuse
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}
</code></pre>
<h3>Why Each Header Matters</h3>
<strong>Host header</strong>: Backend needs to know which virtual host was requested
<pre><code class="language-nginx">proxy_set_header Host $host;  # Preserve original Host header
</code></pre>
<p>Without this, backend sees Nginx's internal hostname, breaking virtual hosting.</p>
<strong>X-Real-IP</strong>: Backend can't see client IP otherwise
<pre><code class="language-nginx">proxy_set_header X-Real-IP $remote_addr;
</code></pre>
<p>Backend sees Nginx's IP without this, breaking IP-based access control and logging.</p>
<strong>X-Forwarded-For</strong>: Preserves proxy chain
<pre><code class="language-nginx">proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</code></pre>
<p>Appends client IP to existing header, maintaining full proxy chain.</p>
<strong>X-Forwarded-Proto</strong>: Backend needs to know if request was HTTPS
<pre><code class="language-nginx">proxy_set_header X-Forwarded-Proto $scheme;
</code></pre>
<p>Critical for backends that generate absolute URLs or enforce HTTPS.</p>
<p>---</p>
<h2>Load Balancing Strategies</h2>
<p>!<a href="/blog/nginx/nginx-load-balancing.png">Load Balancing with Nginx</a></p>
<p>Nginx provides several load balancing algorithms, each suited for different scenarios.</p>
<h3>Upstream Block Configuration</h3>
<pre><code class="language-nginx">upstream backend {
    # Load balancing method (see below)
    least_conn;
    
    # Backend servers
    server app1:8080 weight=3 max_fails=3 fail_timeout=30s;
    server app2:8080 weight=2 max_fails=3 fail_timeout=30s;
    server app3:8080 weight=1 max_fails=3 fail_timeout=30s;
    server app4:8080 backup;  # Only used if all others down
    
    # Connection pooling
    keepalive 32;
    keepalive_requests 100;
    keepalive_timeout 60s;
}
<p>location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}
</code></pre></p>
<h3>Load Balancing Algorithms</h3>
<strong>1. Round Robin (default)</strong>
<pre><code class="language-nginx">upstream backend {
    server app1:8080;
    server app2:8080;
    server app3:8080;
}
</code></pre>
<ul><li>Distributes requests evenly across servers</li>
<li>Simple, predictable, works well for homogeneous backends</li>
<li>Use when: All backends have equal capacity</li>
</ul>
<strong>2. Least Connections</strong>
<pre><code class="language-nginx">upstream backend {
    least_conn;
    server app1:8080;
    server app2:8080;
    server app3:8080;
}
</code></pre>
<ul><li>Routes to server with fewest active connections</li>
<li>Better for long-lived connections or mixed workloads</li>
<li>Use when: Request processing time varies significantly</li>
</ul>
<strong>3. IP Hash</strong>
<pre><code class="language-nginx">upstream backend {
    ip_hash;
    server app1:8080;
    server app2:8080;
    server app3:8080;
}
</code></pre>
<ul><li>Same client IP always goes to same backend</li>
<li>Provides session affinity without shared storage</li>
<li>Use when: Application requires sticky sessions but can't use shared session store</li>
<li><strong>Limitation</strong>: Breaks if client IP changes (mobile networks)</li>
</ul>
<strong>4. Generic Hash</strong>
<pre><code class="language-nginx">upstream backend {
    hash $request_uri consistent;  # Or $cookie_sessionid, etc.
    server app1:8080;
    server app2:8080;
    server app3:8080;
}
</code></pre>
<ul><li>Hash any variable for custom affinity</li>
<li></code>consistent<code> parameter uses consistent hashing (minimizes redistribution when servers change)</li>
<li>Use when: You need affinity based on specific request attributes</li>
</ul>
<strong>5. Least Time (Nginx Plus only)</strong>
<pre><code class="language-nginx">upstream backend {
    least_time header;  # Or 'last_byte'
    server app1:8080;
    server app2:8080;
    server app3:8080;
}
</code></pre>
<ul><li>Routes to server with lowest average response time</li>
<li>Most intelligent algorithm, adapts to backend performance</li>
<li>Use when: You have Nginx Plus and mixed backend performance</li>
</ul>
<h3>Health Checks and Failure Handling</h3>
<pre><code class="language-nginx">upstream backend {
    server app1:8080 max_fails=3 fail_timeout=30s;
    server app2:8080 max_fails=3 fail_timeout=30s;
}
</code></pre>
<strong>Passive health checks</strong>:
<ul><li>After </code>max_fails<code> consecutive failures, mark server as down</li>
<li>Retry after </code>fail_timeout<code> seconds</li>
<li>Failure criteria: Connection refused, timeout, or 502/503/504 response</li>
</ul>
<strong>Active health checks (Nginx Plus)</strong>:
<pre><code class="language-nginx">upstream backend {
    zone backend 64k;
    server app1:8080;
    server app2:8080;
}
<p>location / {
    proxy_pass http://backend;
    health_check interval=5s fails=3 passes=2 uri=/health;
}
</code></pre></p>
<p>Periodically sends health check requests regardless of client traffic.</p>
<h3>Connection Pooling</h3>
<pre><code class="language-nginx">upstream backend {
    server app1:8080;
    server app2:8080;
    
    keepalive 32;  # Pool size
    keepalive_requests 100;  # Requests per connection
    keepalive_timeout 60s;  # Idle timeout
}
<p>location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;  # Required
    proxy_set_header Connection "";  # Clear Connection header
}
</code></pre></p>
<strong>Impact</strong>: Reduces TCP handshake overhead, especially over high-latency networks.
<p>---</p>
<h2>Advanced Reverse Proxy Patterns</h2>
<p>!<a href="/blog/nginx/nginx-reverse-proxy-diagram.jpg">Reverse Proxy Diagram</a></p>
<h3>Request/Response Buffering</h3>
<p>By default, Nginx buffers backend responses before sending to client:</p>
<pre><code class="language-nginx">proxy_buffering on;  # Default
proxy_buffer_size 4k;  # Buffer for response headers
proxy_buffers 8 4k;  # Buffers for response body
proxy_busy_buffers_size 8k;  # Can send to client while receiving
</code></pre>
<strong>Advantages</strong>:
<ul><li>Frees backend quickly (backend doesn't wait for slow clients)</li>
<li>Smoother traffic with slow clients</li>
<li>Can retry on backend failure</li>
</ul>
<strong>Disadvantages</strong>:
<ul><li>Adds latency for first byte</li>
<li>Uses more memory</li>
<li>Breaks Server-Sent Events, WebSockets, streaming</li>
</ul>
<h3>When to Disable Buffering</h3>
<pre><code class="language-nginx">location /stream {
    proxy_buffering off;  # Disable for streaming
    proxy_pass http://backend;
}
</code></pre>
<p>Use </code>proxy_buffering off<code> for:
<ul><li>Server-Sent Events (SSE)</li>
<li>WebSockets (after upgrade)</li>
<li>Streaming APIs</li>
<li>Large file downloads where you want backpressure</li>
</ul>
<h3>Request Buffering</h3></p>
<pre><code class="language-nginx">client_body_buffer_size 128k;  # Buffer requests in memory
client_max_body_size 100m;  # Max request body size
client_body_temp_path /tmp/nginx/client_body;  # Disk spill location
</code></pre>
<strong>Large uploads</strong>: Requests > buffer size spill to disk. Set </code>client_body_buffer_size<code> based on typical request sizes.
<p>---</p>
<h2>TLS/SSL Configuration</h2>
<p>!<a href="/blog/nginx/nginx-ssl-termination.png">TLS Termination</a></p>
<p>Nginx commonly terminates TLS, offloading encryption from application servers.</p>
<h3>Modern TLS Configuration</h3>
<pre><code class="language-nginx">server {
    listen 443 ssl http2;
    server_name example.com;
    
    # Certificate and key
    ssl_certificate /etc/ssl/certs/fullchain.pem;
    ssl_certificate_key /etc/ssl/private/privkey.pem;
    
    # Protocols
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    
    # Ciphers
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    
    # Session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets off;
    
    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/certs/chain.pem;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
</code></pre>
<h3>SSL Session Caching</h3>
<pre><code class="language-nginx">ssl_session_cache shared:SSL:10m;
</code></pre>
<p>Caches SSL session parameters across workers:
<ul><li>Eliminates full handshake for returning clients</li>
<li>Reduces CPU usage by ~90% for repeat connections</li>
<li>1MB cache stores ~4000 sessions</li>
</ul>
<h3>OCSP Stapling</h3></p>
<pre><code class="language-nginx">ssl_stapling on;
ssl_stapling_verify on;
</code></pre>
<p>Nginx fetches OCSP responses and includes them in TLS handshake:
<ul><li>Clients don't need to contact CA</li>
<li>Faster connections, better privacy</li>
<li>Reduces CA load</li>
</ul>
---</p>
<h2>HTTP/2 Optimization</h2>
<p>Enable HTTP/2 for better performance:</p>
<pre><code class="language-nginx">listen 443 ssl http2;
</code></pre>
<strong>Benefits</strong>:
<ul><li>Multiplexing: Multiple requests over one TCP connection</li>
<li>Header compression: Reduces bandwidth</li>
<li>Server push: Proactively send resources (use sparingly)</li>
</ul>
<strong>Note</strong>: HTTP/2 only works over TLS in practice (browsers require it).
<p>---</p>
<h2>Caching Configuration</h2>
<p>!<a href="/blog/nginx/nginx-cache-1.jpg">Nginx Caching</a></p>
<p>Nginx can cache proxy responses, dramatically reducing backend load.</p>
<h3>Caching Setup</h3>
<pre><code class="language-nginx"><h1>Define cache zone</h1>
proxy_cache_path /var/cache/nginx/proxy
                 levels=1:2
                 keys_zone=my_cache:10m
                 max_size=1g
                 inactive=60m
                 use_temp_path=off;
<p>server {
    location / {
        proxy_cache my_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        
        # Cache key
        proxy_cache_key "$scheme$request_method$host$request_uri";
        
        # Add cache status header
        add_header X-Cache-Status $upstream_cache_status;
        
        proxy_pass http://backend;
    }
}
</code></pre></p>
<h3>Cache Parameters Explained</h3>
<strong>levels=1:2</strong>: Directory structure depth (prevents too many files in one directory)
<strong>keys_zone</strong>: Shared memory zone for cache metadata (1MB ≈ 8000 keys)
<strong>max_size</strong>: Maximum cache size on disk
<strong>inactive</strong>: Remove cached items not accessed in this period
<strong>use_temp_path=off</strong>: Write directly to cache directory (faster)
<h3>Cache Behavior Control</h3>
<pre><code class="language-nginx">proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
</code></pre>
<p>Serve stale cache when:
<ul><li>Backend is down</li>
<li>Backend times out</li>
<li>Backend is being updated</li>
<li>Backend returns 500/502/503</li>
</ul>
This provides better availability than failing.</p>
<h3>Cache Bypass</h3>
<pre><code class="language-nginx">proxy_cache_bypass $http_pragma $http_authorization;
proxy_no_cache $http_pragma $http_authorization;
</code></pre>
<p>Don't cache requests with Pragma or Authorization headers.</p>
<h3>Cache Performance</h3>
<p>!<a href="/blog/nginx/nginx-cache-2.png">Cache Architecture</a></p>
<p>With caching:
<ul><li>Cache hit: ~0.1ms response time, 0% backend load</li>
<li>Cache miss: Normal backend latency</li>
<li>Cache hit ratio determines overall performance gain</li>
</ul>
Monitor with:</p>
<pre><code class="language-nginx">add_header X-Cache-Status $upstream_cache_status;
</code></pre>
<p>Values: HIT, MISS, EXPIRED, STALE, UPDATING, REVALIDATED, BYPASS</p>
<p>---</p>
<h2>Rate Limiting</h2>
<p>Nginx provides sophisticated rate limiting to protect backends and enforce quotas.</p>
<h3>Basic Rate Limiting</h3>
<pre><code class="language-nginx"><h1>Define rate limit zone</h1>
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
<p>location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://backend;
}
</code></pre></p>
<strong>Parameters</strong>:
<ul><li></code>$binary_remote_addr<code>: Key for limiting (client IP)</li>
<li></code>zone=api:10m<code>: 10MB shared memory zone (stores ~160k IP addresses)</li>
<li></code>rate=10r/s<code>: Allow 10 requests per second</li>
<li></code>burst=20<code>: Allow bursts up to 20 requests above rate</li>
<li></code>nodelay<code>: Process burst immediately (don't delay to smooth rate)</li>
</ul>
<h3>Rate Limiting Behavior</h3>
<p>Without </code>burst<code>:
<ul><li>Request 1-10 in first second: OK</li>
<li>Request 11: 503 (rate exceeded)</li>
</ul>
With </code>burst=20 nodelay<code>:
<ul><li>Request 1-30 in first second: All OK (10 + 20 burst)</li>
<li>Request 31: 503</li>
<li>After 1 second: 10 more requests allowed (rate refills)</li>
</ul>
With </code>burst=20<code> (no nodelay):
<ul><li>Request 1-10: Served immediately</li>
<li>Request 11-30: Queued, served at rate (100ms apart)</li>
<li>Request 31: 503</li>
</ul>
<h3>Multiple Rate Limits</h3></p>
<pre><code class="language-nginx">limit_req_zone $binary_remote_addr zone=global:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $server_name zone=per_vhost:10m rate=1000r/s;
<p>location /api/ {
    limit_req zone=global burst=50;
    limit_req zone=api burst=20;
    proxy_pass http://backend;
}
</code></pre></p>
<p>All limits must pass for request to proceed.</p>
<h3>Connection Limiting</h3>
<pre><code class="language-nginx">limit_conn_zone $binary_remote_addr zone=addr:10m;
<p>location /download/ {
    limit_conn addr 2;  # Max 2 concurrent connections per IP
    proxy_pass http://storage;
}
</code></pre></p>
<p>Use for:
<ul><li>Limiting concurrent downloads</li>
<li>Preventing resource exhaustion</li>
<li>Enforcing connection quotas</li>
</ul>
---</p>
<h2>Security Best Practices</h2>
<h3>Hide Server Information</h3>
<pre><code class="language-nginx">server_tokens off;  # Don't expose Nginx version
more_clear_headers Server;  # Remove Server header entirely (requires headers-more module)
</code></pre>
<h3>Deny Access to Hidden Files</h3>
<pre><code class="language-nginx">location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
}
</code></pre>
<p>Blocks </code>.git<code>, </code>.env<code>, </code>.htaccess<code>, etc.</p>
<h3>Block Common Vulnerability Scanners</h3>
<pre><code class="language-nginx">location ~* (wp-admin|wp-login|xmlrpc\.php|phpmyadmin) {
    return 444;  # Close connection without response
}
</code></pre>
<h3>Request Size Limits</h3>
<pre><code class="language-nginx">client_max_body_size 10m;  # Max request body
client_body_timeout 12s;
client_header_timeout 12s;
</code></pre>
<p>Prevents slowloris and similar attacks.</p>
<h3>Security Headers</h3>
<pre><code class="language-nginx">add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' https:" always;
</code></pre>
<p>---</p>
<h2>Zero-Downtime Operations</h2>
<h3>Configuration Reload</h3>
<pre><code class="language-bash">nginx -t  # Test configuration
nginx -s reload  # Graceful reload
</code></pre>
<strong>What happens</strong>:
<ul><li>Master reads new config</li>
<li>Master spawns new workers</li>
<li>Master signals old workers to stop accepting new connections</li>
<li>Old workers finish existing requests</li>
<li>Old workers exit after all requests complete</li>
<li>No connections dropped</li>
</ul>
<h3>Binary Upgrade</h3>
<pre><code class="language-bash">kill -USR2 </code>cat /var/run/nginx.pid<code>  # Start new master with new binary
kill -WINCH </code>cat /var/run/nginx.pid.oldbin<code>  # Old workers stop accepting connections
kill -QUIT </code>cat /var/run/nginx.pid.oldbin<code>  # Gracefully shutdown old master
</code></pre>
<p>Upgrades Nginx binary without downtime.</p>
<p>---</p>
<h2>Observability and Debugging</h2>
<h3>Structured Logging</h3>
<pre><code class="language-nginx">log_format detailed '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';
<p>access_log /var/log/nginx/access.log detailed buffer=32k flush=5s;
error_log /var/log/nginx/error.log warn;
</code></pre></p>
<strong>Key metrics</strong>:
<ul><li></code>$request_time<code>: Total request time (client → Nginx → backend → client)</li>
<li></code>$upstream_connect_time<code>: Time to connect to backend</li>
<li></code>$upstream_header_time<code>: Time to receive first byte from backend</li>
<li></code>$upstream_response_time<code>: Time to receive full response from backend</li>
</ul>
<h3>Debug Logging</h3>
<pre><code class="language-nginx">error_log /var/log/nginx/error.log debug;
</code></pre>
<p>Extremely verbose, use only for troubleshooting specific issues.</p>
<h3>Stub Status Module</h3>
<pre><code class="language-nginx">location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}
</code></pre>
<pre><code class="language-bash">curl http://localhost/nginx_status
</code></pre>
<p>Output:
</code>`<code>
Active connections: 291
server accepts handled requests
 16630 16630 31070
Reading: 6 Writing: 179 Waiting: 106
</code>``</p>
<p>---</p>
<h2>Common Production Mistakes</h2>
<strong>1. Too many regex locations</strong> - Regex matching is sequential, slow for many patterns
<strong>2. Missing timeouts</strong> - Slow backends can exhaust connections
<pre><code class="language-nginx">proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
</code></pre>
<strong>3. Over-buffering</strong> - Wastes memory, increases latency
<strong>4. Ignoring worker_connections</strong> - Limits concurrent connections
<pre><code class="language-nginx">events {
    worker_connections 4096;  # Adjust based on load
}
</code></pre>
<strong>5. Not using connection pooling</strong> to upstreams
<strong>6. Forgetting to set proxy headers</strong> (Host, X-Real-IP, etc.)
<strong>7. Using if in location context</strong> - Often doesn't work as expected
<pre><code class="language-nginx"><h1>Bad</h1>
location / {
    if ($request_method = POST) {
        proxy_pass http://backend;
    }
}
<h1>Good</h1>
location = /api {
    limit_except GET HEAD {
        proxy_pass http://backend;
    }
}
</code></pre>
<p>---</p>
<h2>Performance Tuning</h2>
<h3>Worker Configuration</h3>
<pre><code class="language-nginx">worker_processes auto;  # One per CPU core
worker_rlimit_nofile 65535;  # File descriptor limit
worker_cpu_affinity auto;  # Pin workers to CPUs
<p>events {
    worker_connections 4096;  # Max concurrent connections per worker
    use epoll;  # Linux kernel 2.6+
    multi_accept on;  # Accept multiple connections per event loop iteration
}
</code></pre></p>
<h3>TCP Optimization</h3>
<pre><code class="language-nginx">http {
    sendfile on;  # Use sendfile() for zero-copy
    tcp_nopush on;  # Optimize packet sizes
    tcp_nodelay on;  # Disable Nagle's algorithm for low latency
    
    keepalive_timeout 65;  # Client connection reuse
    keepalive_requests 100;
    
    reset_timedout_connection on;  # Reset timed out connections
}
</code></pre>
<h3>File Caching</h3>
<pre><code class="language-nginx">open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
</code></pre>
<p>Caches open file descriptors for static files.</p>
<p>---</p>
<h2>Nginx in Microservices Architecture</h2>
<p>Nginx often serves as:</p>
<ul><li><strong>API Gateway</strong> - Single entry point, routing, authentication</li>
<li><strong>Service Mesh Sidecar</strong> - Per-service proxy (though Envoy more common now)</li>
<li><strong>Ingress Controller</strong> (Kubernetes) - External traffic routing</li>
<li><strong>Load Balancer</strong> - Distributing traffic across service instances</li>
</ul>
<h3>Kubernetes Ingress Example</h3>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1/users
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 8080
</code></pre>
<p>Nginx Ingress Controller converts this to Nginx config.</p>
<p>---</p>
<h2>When NOT to Use Nginx</h2>
<p>Nginx is infrastructure, not application logic. Don't use Nginx for:</p>
<ul><li><strong>Complex business logic</strong> - Use application code</li>
<li><strong>Dynamic routing decisions</strong> - Use API gateway or service mesh</li>
<li><strong>Stateful request handling</strong> - Nginx is stateless</li>
<li><strong>Heavy data transformation</strong> - Use application tier</li>
<li><strong>Database queries</strong> - Obviously belongs in application</li>
</ul>
<strong>Rule</strong>: If it requires understanding your domain model, it doesn't belong in Nginx.
<p>---</p>
<h2>Conclusion</h2>
<p>Nginx is a high-performance traffic control layer that, when properly configured, becomes one of the most reliable components in your infrastructure.</p>
<strong>Key takeaways</strong>:
<ul><li><strong>Architecture matters</strong> - Event-driven design enables massive concurrency</li>
<li><strong>Request flow understanding</strong> - Know the 11 processing phases</li>
<li><strong>Location matching</strong> - Most misunderstood feature, most important to get right</li>
<li><strong>Headers in proxy</strong> - Always set Host, X-Real-IP, X-Forwarded-*</li>
<li><strong>Load balancing</strong> - Choose algorithm based on your workload</li>
<li><strong>Caching</strong> - Dramatic performance gains for read-heavy workloads</li>
<li><strong>Security</strong> - Rate limiting, request size limits, security headers</li>
<li><strong>Zero-downtime</strong> - Graceful reloads and binary upgrades</li>
<li><strong>Observability</strong> - Log timing breakdowns, monitor cache hit rates</li>
<li><strong>Know your limits</strong> - Nginx is infrastructure, not application logic</li>
</ul>
          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> January 15, 2026<br/>
            <strong>Reading Time:</strong> 20 min read<br/>
            <strong>Tags:</strong> nginx, web-server, reverse-proxy, load-balancing, devops, performance, configuration<br/>
            <a href="https://www.modev.me/blog/nginx-deep-dive-architecture-configuration-production-patterns" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/nginx-deep-dive-architecture-configuration-production-patterns</link>
      <guid isPermaLink="true">https://www.modev.me/blog/nginx-deep-dive-architecture-configuration-production-patterns</guid>
      <pubDate>Thu, 15 Jan 2026 10:00:00 GMT</pubDate>
      <lastBuildDate>Thu, 15 Jan 2026 10:00:00 GMT</lastBuildDate>
      <category>nginx</category>
      <category>web-server</category>
      <category>reverse-proxy</category>
      <category>load-balancing</category>
      <category>devops</category>
      <category>performance</category>
      <category>configuration</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/nginx-deep-dive-architecture-configuration-production-patterns" type="image/png" length="0"/>
    </item>

    <item>
      <title><![CDATA[SimuKernel OS Concepts Explained]]></title>
      <description><![CDATA[A practical guide to CPU scheduling, memory management, and process control using SimuKernel an educational operating system simulator.]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/simukernel-operating-system-concepts" alt="SimuKernel OS Concepts Explained" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>OS Concepts Explained</h1>
<p>This article provides detailed explanations of the operating system concepts implemented in SimuKernel.</p>
<p>> Source code: https://github.com/Mo7ammedd/SimuKernel</p>

<h2>CPU Scheduling</h2>
<h3>What is CPU Scheduling?</h3>
<p>CPU scheduling is the process of determining which process in the ready queue will be allocated the CPU for execution. The goal is to maximize CPU utilization, throughput, and minimize waiting time and response time.</p>
<h3>Process States</h3>
<p>A process transitions through several states:
<ul><li><strong>New</strong>: Process is being created</li>
<li><strong>Ready</strong>: Process is waiting to be assigned to CPU</li>
<li><strong>Running</strong>: Instructions are being executed</li>
<li><strong>Waiting</strong>: Process is waiting for I/O or event</li>
<li><strong>Terminated</strong>: Process has finished execution</li>
</ul>
<h3>Scheduling Algorithms</h3></p>
<h4>1. Round Robin (RR)</h4>
<strong>How it works:</strong>
<ul><li>Each process gets a fixed time slice (quantum)</li>
<li>After quantum expires, process moves to end of ready queue</li>
<li>Next process in queue gets CPU</li>
<li>Preemptive algorithm</li>
</ul>
<strong>Advantages:</strong>
<ul><li>Fair allocation of CPU time</li>
<li>No starvation</li>
<li>Good for time-sharing systems</li>
<li>Predictable response time</li>
</ul>
<strong>Disadvantages:</strong>
<ul><li>Context switching overhead</li>
<li>Average waiting time can be high</li>
<li>Performance depends on time quantum selection</li>
</ul>
<strong>Time Quantum Selection:</strong>
<ul><li>Too small: High context switching overhead</li>
<li>Too large: Becomes FCFS, poor response time</li>
<li>Typical: 10-100 milliseconds</li>
</ul>
<strong>Example:</strong>
``<code>
Processes: P1(burst=10), P2(burst=5), P3(burst=8)
Quantum = 4
<p>Execution Order:
P1[0-4] → P2[4-8] → P3[8-12] → P1[12-16] → P2[16-17] → P3[17-21] → P1[21-22]
</code>`<code></p>
<h4>2. Priority Scheduling</h4>
<strong>How it works:</strong>
<ul><li>Each process has a priority number</li>
<li>CPU allocated to highest priority process</li>
<li>Can be preemptive or non-preemptive</li>
<li>Lower number = higher priority (convention)</li>
</ul>
<strong>Preemptive Priority:</strong>
<ul><li>Running process can be interrupted</li>
<li>New higher priority process gets CPU immediately</li>
</ul>
<strong>Non-Preemptive Priority:</strong>
<ul><li>Process runs to completion</li>
<li>Priority checked only when CPU becomes free</li>
</ul>
<strong>Advantages:</strong>
<ul><li>Important processes execute first</li>
<li>Good for real-time systems</li>
<li>Flexible based on process needs</li>
</ul>
<strong>Disadvantages:</strong>
<ul><li><strong>Starvation</strong>: Low priority processes may never execute</li>
<li>Priority inversion in complex systems</li>
</ul>
<strong>Starvation Prevention:</strong>
<ul><li>Aging: Gradually increase priority of waiting processes</li>
<li>After threshold time, boost priority</li>
</ul>
<strong>Example:</strong>
</code>`<code>
Processes: P1(priority=3), P2(priority=1), P3(priority=4), P4(priority=2)
<p>Non-Preemptive Order: P2 → P4 → P1 → P3
(Execution order by priority)
</code>`<code></p>
<h4>3. Multilevel Feedback Queue (MLFQ)</h4>
<strong>How it works:</strong>
<ul><li>Multiple queues with different priorities</li>
<li>Each queue has its own scheduling algorithm (usually RR)</li>
<li>Different time quantums for each level</li>
<li>Processes can move between queues</li>
</ul>
<strong>Queue Structure:</strong>
</code>`<code>
Queue 0 (Highest Priority) - Quantum: 2ms
Queue 1 (Medium Priority)  - Quantum: 4ms  
Queue 2 (Lowest Priority)  - Quantum: 8ms
</code>`<code>
<strong>Process Movement Rules:</strong>
<ul><li>New processes enter highest priority queue</li>
<li>If process uses entire quantum: Move down one level</li>
<li>If process blocks (I/O): Stay in same level or move up</li>
<li>If process waits too long (aging): Move up one level</li>
</ul>
<strong>Advantages:</strong>
<ul><li>Favors short jobs (I/O-bound)</li>
<li>Prevents starvation through aging</li>
<li>Adapts to process behavior</li>
<li>No need to know burst time in advance</li>
</ul>
<strong>Disadvantages:</strong>
<ul><li>Complex to implement</li>
<li>Requires parameter tuning</li>
<li>Can have high overhead</li>
</ul>
<strong>Real-World Use:</strong>
<ul><li>Linux CFS (Completely Fair Scheduler)</li>
<li>Windows Thread Scheduler</li>
<li>macOS Grand Central Dispatch</li>
</ul>
---
<h2>Memory Management</h2>
<h3>Virtual Memory</h3>
<strong>Concept:</strong>
<ul><li>Separation of logical and physical memory</li>
<li>Processes use virtual addresses</li>
<li>MMU (Memory Management Unit) translates to physical addresses</li>
<li>Allows running programs larger than physical RAM</li>
</ul>
<strong>Benefits:</strong>
<ul><li>Process isolation (security)</li>
<li>Efficient memory use</li>
<li>Simplified programming model</li>
<li>Support for shared libraries</li>
</ul>
<h3>Paging</h3>
<strong>How it works:</strong>
<ul><li>Divide memory into fixed-size blocks</li>
<li><strong>Pages</strong>: Logical memory blocks (process view)</li>
<li><strong>Frames</strong>: Physical memory blocks (hardware view)</li>
<li>Page table maps pages to frames</li>
</ul>
<strong>Page Table:</strong>
</code>`<code>
Virtual Page → Physical Frame
    0       →      2
    1       →      5
    2       →      1
    3       →      7
</code>`<code>
<strong>Address Translation:</strong>
</code>`<code>
Virtual Address = Page Number + Offset
Physical Address = Frame Number + Offset
<p>Example:
Virtual: Page 2, Offset 100
Physical: Frame 1, Offset 100
</code>`<code></p>
<h3>Page Replacement Algorithms</h3>
<p>When memory is full and new page needed, must replace existing page.</p>
<h4>1. FIFO (First-In-First-Out)</h4>
<strong>Algorithm:</strong>
<ul><li>Maintain queue of pages in memory</li>
<li>Replace oldest page (first loaded)</li>
</ul>
<strong>Implementation:</strong>
</code>`<code>
Queue: [P1, P2, P3, P4]
New page P5 arrives → Replace P1
Queue: [P2, P3, P4, P5]
</code>`<code>
<strong>Advantages:</strong>
<ul><li>Simple to implement</li>
<li>Low overhead</li>
<li>Fair (all pages age equally)</li>
</ul>
<strong>Disadvantages:</strong>
<ul><li>Belady's Anomaly: More frames can cause more faults</li>
<li>Doesn't consider page usage patterns</li>
<li>May replace frequently used pages</li>
</ul>
<strong>Belady's Anomaly Example:</strong>
</code>`<code>
Reference: 1,2,3,4,1,2,5,1,2,3,4,5
<p>3 Frames: 9 page faults
4 Frames: 10 page faults (worse!)
</code>`<code></p>
<h4>2. LRU (Least Recently Used)</h4>
<strong>Algorithm:</strong>
<ul><li>Replace page not used for longest time</li>
<li>Assumes: Recently used pages will be used again soon</li>
</ul>
<strong>Implementation:</strong>
<ul><li>Timestamp each page access</li>
<li>On replacement, choose minimum timestamp</li>
<li>Or use stack/counter</li>
</ul>
<strong>Advantages:</strong>
<ul><li>Better performance than FIFO</li>
<li>No Belady's anomaly</li>
<li>Approximates optimal</li>
</ul>
<strong>Disadvantages:</strong>
<ul><li>High overhead (tracking access times)</li>
<li>Requires hardware support</li>
<li>Complex implementation</li>
</ul>
<strong>Hardware Support:</strong>
<ul><li>Timestamp register</li>
<li>Stack of page numbers</li>
<li>Counter for each page table entry</li>
</ul>
<strong>Example:</strong>
</code>`<code>
Time: 0  1  2  3  4  5
Ref:  1  2  3  1  4  2
<p>Access times when referencing page 5:
P1: 3, P2: 5, P3: 2, P4: 4
Replace P3 (oldest access)
</code>`<code></p>
<h4>3. Optimal Page Replacement</h4>
<strong>Algorithm:</strong>
<ul><li>Replace page that won't be used for longest time in future</li>
<li>Theoretical optimal (minimum possible faults)</li>
</ul>
<strong>Implementation:</strong>
<ul><li>Look ahead in reference string</li>
<li>For each frame, find next use time</li>
<li>Replace frame with farthest next use</li>
<li>If page never used again, replace it immediately</li>
</ul>
<strong>Advantages:</strong>
<ul><li>Minimum page faults (provably optimal)</li>
<li>Good benchmark for other algorithms</li>
</ul>
<strong>Disadvantages:</strong>
<ul><li>Impossible to implement (requires future knowledge)</li>
<li>Only useful for analysis and comparison</li>
</ul>
<strong>Example:</strong>
</code>`<code>
Reference: 1,2,3,4,1,2,5,1,2,3,4,5
Frames: 3
<p>At step 6 (need to load page 5):
P1: next used at step 7 (distance: 1)
P2: next used at step 8 (distance: 2)  
P3: next used at step 10 (distance: 4)
P4: not used again (distance: ∞)</p>
<p>Replace P4 (optimal choice)
</code>`<code></p>
<h3>Page Fault Handling</h3>
<strong>Page Fault Process:</strong>
<ul><li>CPU tries to access page not in memory</li>
<li>MMU generates page fault interrupt</li>
<li>OS locates page on disk</li>
<li>Selects victim frame (if memory full)</li>
<li>Writes victim page to disk (if modified)</li>
<li>Loads requested page into frame</li>
<li>Updates page table</li>
<li>Restarts instruction</li>
</ul>
<strong>Page Fault Cost:</strong>
<ul><li>Trap to OS: ~1-10 microseconds</li>
<li>Disk access: ~1-10 milliseconds</li>
<li>Total: ~1000x slower than memory access</li>
</ul>
---
<h2>Process Management</h2>
<h3>Process Control Block (PCB)</h3>
<p>Contains process information:
<ul><li>Process ID (PID)</li>
<li>Process state</li>
<li>Program counter</li>
<li>CPU registers</li>
<li>Memory management info</li>
<li>I/O status</li>
<li>Accounting information</li>
</ul>
<h3>Context Switch</h3></p>
<strong>What happens:</strong>
<ul><li>Save state of current process to PCB</li>
<li>Select next process to run</li>
<li>Restore state from new process PCB</li>
<li>Switch to user mode</li>
</ul>
<strong>Cost:</strong>
<ul><li>Direct: Saving/restoring registers</li>
<li>Indirect: Cache/TLB flush, pipeline stall</li>
<li>Typical: 1-10 microseconds</li>
</ul>
<h3>Process Creation</h3>
<strong>Unix/Linux (fork/exec):</strong>
</code>`<code>
parent process
    ↓
fork() → creates child with copy of parent memory
    ↓
exec() → replaces child memory with new program
    ↓
child executes new program
</code>`<code>
<p>---</p>
<h2>Performance Metrics</h2>
<h3>CPU Scheduling Metrics</h3>
<strong>1. Turnaround Time</strong>
</code>`<code>
Turnaround Time = Completion Time - Arrival Time
</code>`<code>
Total time from submission to completion.
<strong>2. Waiting Time</strong>
</code>`<code>
Waiting Time = Turnaround Time - Burst Time
</code>`<code>
Time spent in ready queue.
<strong>3. Response Time</strong>
</code>`<code>
Response Time = First Run Time - Arrival Time
</code>`<code>
Time from submission to first execution.
<strong>4. CPU Utilization</strong>
</code>`<code>
CPU Utilization = (Total CPU Busy Time / Total Time) × 100%
</code>`<code>
Percentage of time CPU is doing useful work.
<strong>5. Throughput</strong>
</code>`<code>
Throughput = Number of Processes / Total Time
</code>`<code>
Processes completed per time unit.
<h3>Memory Management Metrics</h3>
<strong>1. Page Fault Rate</strong>
</code>`<code>
Page Fault Rate = (Number of Page Faults / Total References) × 100%
</code>`<code>
<strong>2. Effective Access Time (EAT)</strong>
</code>`<code>
EAT = (1 - p) × memory_access_time + p × page_fault_time
<p>where p = page fault rate
</code>`<code></p>
<strong>Example:</strong>
<ul><li>Memory access: 100 ns</li>
<li>Page fault time: 10 ms = 10,000,000 ns</li>
<li>Page fault rate: 0.1% = 0.001</li>
</ul>
</code>`<code>
EAT = 0.999 × 100 + 0.001 × 10,000,000
    = 99.9 + 10,000
    = 10,099.9 ns
</code>`<code>
<strong>3. Memory Utilization</strong>
</code>`<code>
Memory Utilization = (Used Memory / Total Memory) × 100%
</code>``
<p>---</p>
<h2>Real-World Applications</h2>
<h3>Linux Process Scheduler</h3>
<strong>CFS (Completely Fair Scheduler):</strong>
<ul><li>Red-black tree of runnable processes</li>
<li>Each process has virtual runtime (vruntime)</li>
<li>Runs process with smallest vruntime</li>
<li>Time slice based on nice value and number of processes</li>
</ul>
<h3>Windows Memory Manager</h3>
<strong>Working Set Management:</strong>
<ul><li>Each process has working set (pages in physical memory)</li>
<li>Minimum and maximum working set sizes</li>
<li>Page frame database tracks all physical memory</li>
<li>Modified page writer writes dirty pages to disk</li>
</ul>
<h3>Android Process Management</h3>
<strong>Low Memory Killer:</strong>
<ul><li>Processes assigned priority levels</li>
<li>When memory low, kills lowest priority processes</li>
<li>Background apps killed before foreground</li>
<li>System services protected</li>
</ul>
---
<h2>Further Reading</h2>
<ul><li><strong>Books:</strong></li>
</ul>  - "Operating Systems: Internals and Design Principles" by William Stallings
  - "Modern Operating Systems" by Andrew Tanenbaum
  - "Operating Systems: Three Easy Pieces" by Remzi H. Arpaci-Dusseau
<ul><li><strong>Online Resources:</strong></li>
</ul>  - <a href="https://wiki.osdev.org/">OSDev Wiki</a>
  - <a href="https://www.kernel.org/doc/">Linux Kernel Documentation</a>
  - <a href="https://pdos.csail.mit.edu/6.828/">MIT 6.828 Operating System Engineering</a>

          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> October 29, 2025<br/>
            <strong>Reading Time:</strong> 9 min read<br/>
            <strong>Tags:</strong> operating-systems, cpu-scheduling, memory-management, process-management<br/>
            <a href="https://www.modev.me/blog/simukernel-operating-system-concepts" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/simukernel-operating-system-concepts</link>
      <guid isPermaLink="true">https://www.modev.me/blog/simukernel-operating-system-concepts</guid>
      <pubDate>Wed, 29 Oct 2025 10:00:00 GMT</pubDate>
      <lastBuildDate>Wed, 29 Oct 2025 10:00:00 GMT</lastBuildDate>
      <category>operating-systems</category>
      <category>cpu-scheduling</category>
      <category>memory-management</category>
      <category>process-management</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/simukernel-operating-system-concepts" type="image/png" length="0"/>
    </item>

    <item>
      <title><![CDATA[3 Ways to Build Custom Middleware in ASP.NET Core]]></title>
      <description><![CDATA[Three practical ways to build custom middleware in ASP.NET Core.]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/3-ways-to-build-custom-middleware-in-aspnet-core" alt="3 Ways to Build Custom Middleware in ASP.NET Core" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>Building Custom Middleware in ASP.NET Core</h1>
<h2>Introduction</h2>
<p>In ASP.NET Core, middleware is the fundamental mechanism that processes every HTTP request and response. Every request that enters the application and every response that leaves it flows through the middleware pipeline.</p>
<p>A middleware component can:</p>
<ul><li>Inspect the incoming request</li>
<li>Modify request headers or body</li>
<li>Decide whether to short-circuit the pipeline</li>
<li>Invoke the next middleware</li>
<li>Modify the outgoing response</li>
</ul>
Because middleware sits at the lowest level of the request lifecycle, it is commonly used for:
<ul><li>Logging and tracing</li>
<li>Authentication and authorization</li>
<li>Rate limiting</li>
<li>Request validation</li>
<li>Performance monitoring</li>
<li>Response transformation</li>
</ul>
Understanding how to design and implement middleware correctly is critical for building maintainable and high-performance ASP.NET Core applications.
<p>---</p>
<h2>Understanding Middleware Basics</h2>
<p>!<a href="/blog/middlewares/aspnetcore-middleware-pipeline-sequence.png">ASP.NET Core Middleware Pipeline Sequence</a></p>
<p>ASP.NET Core uses a <strong>chain of responsibility</strong> model. Each middleware receives a <code>HttpContext</code> and a delegate pointing to the next component in the pipeline.</p>
<p>Each middleware can execute logic in two phases:</p>
<ul><li>Before calling <code>next</code></li>
<li>After the next middleware completes</li>
</ul>
This enables powerful cross-cutting behavior.
<h3>Conceptual Pipeline Flow</h3>
<pre><code class="language-csharp">async Task ProcessRequest(HttpContext context)
{
    // Middleware A (before)
    await MiddlewareA(context, async () =>
    {
        // Middleware B (before)
        await MiddlewareB(context, async () =>
        {
            // Endpoint execution
            await Controller(context);
        });
        // Middleware B (after)
    });
    // Middleware A (after)
}
</code></pre>
<p>Key observations:</p>
<ul><li>Middleware order matters</li>
<li>Short-circuiting stops downstream execution</li>
<li>Exceptions bubble upward unless handled</li>
</ul>
---
<h2>How Middleware Is Registered</h2>
<p>Middleware is registered during application startup using <code>Use</code>, <code>Map</code>, or <code>Run</code>.</p>
<ul><li><code>Use</code> allows calling the next middleware</li>
<li><code>Run</code> terminates the pipeline</li>
<li><code>Map</code> branches the pipeline based on path</li>
</ul>
Execution order is the same as registration order.
<p>---</p>
<h2>Implementation Methods</h2>
<p>ASP.NET Core provides three main ways to build custom middleware. Each serves a different purpose and has different trade-offs.</p>
<p>---</p>
<h3>1. Request Delegates (Inline Middleware)</h3>
<p>This is the simplest way to write middleware. Logic is defined inline using a lambda.</p>
<pre><code class="language-csharp">app.Use(async (context, next) =>
{
    var timer = Stopwatch.StartNew();
    var logger = context.RequestServices.GetService<ILogger<Program>>();
<p>logger?.LogInformation(
        "Processing {Method} {Path}",
        context.Request.Method,
        context.Request.Path
    );</p>
<p>context.Response.OnStarting(() =>
    {
        context.Response.Headers["X-Response-Time"] =
            timer.ElapsedMilliseconds.ToString();
        return Task.CompletedTask;
    });</p>
<p>await next(context);</p>
<p>timer.Stop();</p>
<p>logger?.LogInformation(
        "Completed {Method} {Path} in {ElapsedMs}ms",
        context.Request.Method,
        context.Request.Path,
        timer.ElapsedMilliseconds
    );
});
</code></pre></p>
<p>This approach is useful for:</p>
<ul><li>Simple logging</li>
<li>Header manipulation</li>
<li>Debugging</li>
<li>Rapid prototyping</li>
</ul>
Register <code>OnStarting</code> before calling <code>next</code>: downstream middleware can start writing the response, after which headers become read-only. The header measures elapsed time until headers are sent; the completion log measures the full downstream duration.
<h4>Limitations</h4>
<ul><li>Hard to test</li>
<li>No clear separation of concerns</li>
<li>Grows messy as logic increases</li>
<li>Limited reuse</li>
</ul>
Inline middleware should remain small and focused.
<p>---</p>
<h3>2. Convention-Based Middleware (Class + Extension Method)</h3>
<p>This is the most commonly recommended approach for production code.</p>
<p>It consists of:</p>
<ul><li>A middleware class</li>
<li>A constructor receiving <code>RequestDelegate</code> and dependencies</li>
<li>An <code>InvokeAsync</code> method</li>
<li>An extension method for registration</li>
</ul>
<pre><code class="language-csharp">public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;
    private readonly RequestLoggingOptions _options;
<p>public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger,
        IOptions<RequestLoggingOptions> options)
    {
        _next = next;
        _logger = logger;
        _options = options.Value;
    }</p>
<p>public async Task InvokeAsync(HttpContext context)
    {
        var request = await FormatRequest(context.Request);
        _logger.LogInformation("Incoming Request: {Request}", request);</p>
<p>await _next(context);</p>
<p>var response = await FormatResponse(context.Response);
        _logger.LogInformation("Outgoing Response: {Response}", response);
    }
}
</code></pre></p>
<h4>Extension Method</h4>
<pre><code class="language-csharp">public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLoggingMiddleware>();
    }
}
</code></pre>
<h4>Usage</h4>
<pre><code class="language-csharp">app.UseRequestLogging();
</code></pre>
<h4>Why This Works Well</h4>
<ul><li>Clear responsibility boundaries</li>
<li>Full dependency injection support</li>
<li>Configurable via options</li>
<li>Easy to unit test</li>
<li>Reusable across applications</li>
</ul>
This is the preferred approach for most custom middleware.
<p>---</p>
<h3>3. Factory-Based Middleware (<code>IMiddleware</code>)</h3>
<p>This approach uses the <code>IMiddleware</code> interface. Each request receives a fresh instance.</p>
<pre><code class="language-csharp">public class PerformanceMiddleware : IMiddleware
{
    private readonly ILogger<PerformanceMiddleware> _logger;
    private readonly IMetricsService _metrics;
<p>public PerformanceMiddleware(
        ILogger<PerformanceMiddleware> logger,
        IMetricsService metrics)
    {
        _logger = logger;
        _metrics = metrics;
    }</p>
<p>public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var timer = Stopwatch.StartNew();
        var path = context.Request.Path;</p>
<p>try
        {
            await next(context);
        }
        finally
        {
            timer.Stop();</p>
<p>await _metrics.RecordMetricAsync(new RequestMetric
            {
                Path = path,
                Method = context.Request.Method,
                Duration = timer.ElapsedMilliseconds,
                StatusCode = context.Response.StatusCode
            });
        }
    }
}
</code></pre></p>
<h4>Registration</h4>
<pre><code class="language-csharp">services.AddTransient<PerformanceMiddleware>();
app.UseMiddleware<PerformanceMiddleware>();
</code></pre>
<h4>Characteristics</h4>
<ul><li>Full constructor injection</li>
<li>New instance per request</li>
<li>Easier unit testing</li>
<li>Slightly higher allocation cost</li>
</ul>
This approach is ideal for complex middleware with many dependencies or when strict test isolation is required.
<p>---</p>
<h2>Middleware Lifetime and DI Behavior</h2>
<ul><li>Convention-based middleware is created once</li>
<li>Dependencies follow their registered lifetimes</li>
<li><code>IMiddleware</code> instances are created per request</li>
</ul>
Avoid injecting scoped services into singleton middleware unless the middleware itself is scoped via <code>IMiddleware</code>.
<p>---</p>
<h2>Best Practices</h2>
<h3>Error Handling</h3>
<p>Centralized exception handling middleware is common.</p>
<pre><code class="language-csharp">public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unhandled exception");
        context.Response.StatusCode = 500;
        await context.Response.WriteAsync("Internal Server Error");
    }
}
</code></pre>
<p>This should be placed early in the pipeline.</p>
<p>---</p>
<h3>Performance Considerations</h3>
<ul><li>Avoid buffering request bodies unless necessary</li>
<li>Use async APIs exclusively</li>
<li>Minimize allocations</li>
<li>Avoid synchronous I/O</li>
<li>Be careful with large response interception</li>
</ul>
Middleware runs on every request. Even small inefficiencies multiply quickly.
<p>---</p>
<h3>Configuration Support</h3>
<p>Middleware should be configurable via options.</p>
<pre><code class="language-csharp">public class MiddlewareOptions
{
    public bool EnableLogging { get; set; }
    public string[] ExcludedPaths { get; set; }
    public int TimeoutSeconds { get; set; }
}
</code></pre>
<pre><code class="language-csharp">services.Configure<MiddlewareOptions>(
    configuration.GetSection("Middleware"));
</code></pre>
<p>Avoid hardcoded behavior.</p>
<p>---</p>
<h2>Advanced Scenarios</h2>
<h3>Conditional Execution</h3>
<pre><code class="language-csharp">public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    if (!_options.ExcludedPaths.Contains(context.Request.Path))
    {
        await ProcessRequest(context);
    }
<p>await next(context);
}
</code></pre></p>
<p>This is useful for excluding health checks or static files.</p>
<p>---</p>
<h3>Branching Pipelines</h3>
<pre><code class="language-csharp">app.Map("/api", apiApp =>
{
    apiApp.UseMiddleware<ApiVersionMiddleware>();
    apiApp.UseMiddleware<ApiKeyMiddleware>();
});
</code></pre>
<p>Branching avoids unnecessary middleware execution for unrelated routes.</p>
<p>---</p>
<h3>Ordering Pitfalls</h3>
<p>Incorrect ordering can break applications.</p>
<p>Examples:</p>
<ul><li>Authentication must run before authorization</li>
<li>Exception handling must wrap downstream components</li>
<li>Response compression must run before response writing</li>
</ul>
Pipeline order should be intentional and documented.
<p>---</p>
<h2>Middleware Approach Comparison</h2>
<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Complexity</th>
      <th>DI Support</th>
      <th>Best Use Case</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Request Delegates</td>
      <td>Low</td>
      <td>Limited</td>
      <td>Small logic, quick checks</td>
    </tr>
    <tr>
      <td>Convention-Based</td>
      <td>Medium</td>
      <td>Good</td>
      <td>Reusable, configurable middleware</td>
    </tr>
    <tr>
      <td>Factory-Based</td>
      <td>High</td>
      <td>Excellent</td>
      <td>Complex logic, enterprise systems</td>
    </tr>
  </tbody>
</table>
<p>---</p>
<h2>Summary</h2>
<p>Middleware is the backbone of ASP.NET Core request processing.</p>
<p>Key takeaways:</p>
<ul><li>Middleware order defines behavior</li>
<li>Keep middleware focused and small</li>
<li>Prefer convention-based middleware by default</li>
<li>Use <code>IMiddleware</code> for complex, test-heavy scenarios</li>
<li>Treat middleware as infrastructure, not business logic</li>
</ul>
Well-designed middleware leads to cleaner controllers, consistent cross-cutting behavior, and scalable applications.
<h2>Verified Examples</h2>
<p>Checked on <strong>September 16, 2026</strong> with <strong>ASP.NET Core 10.0.12</strong> and <strong>.NET 10.0.12</strong>, using SDK 10.0.112. A local Kestrel server handled two real HTTP requests to verify delegate ordering, one-time construction of conventional middleware, per-request resolution of transient <code>IMiddleware</code>, and a response timing header registered through <code>OnStarting</code>.</p>
<p>The verification harness is <code>scripts/verify-blog/dotnet/Program.cs</code> in the portfolio repository. The logging and metrics helpers in the article remain application-specific examples.</p>
          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> March 15, 2024<br/>
            <strong>Reading Time:</strong> 7 min read<br/>
            <strong>Tags:</strong> aspnet-core, middleware, dotnet, web-development, request-pipeline, csharp<br/>
            <a href="https://www.modev.me/blog/3-ways-to-build-custom-middleware-in-aspnet-core" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/3-ways-to-build-custom-middleware-in-aspnet-core</link>
      <guid isPermaLink="true">https://www.modev.me/blog/3-ways-to-build-custom-middleware-in-aspnet-core</guid>
      <pubDate>Fri, 15 Mar 2024 10:00:00 GMT</pubDate>
      <lastBuildDate>Wed, 16 Sep 2026 00:00:00 GMT</lastBuildDate>
      <category>aspnet-core</category>
      <category>middleware</category>
      <category>dotnet</category>
      <category>web-development</category>
      <category>request-pipeline</category>
      <category>csharp</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/3-ways-to-build-custom-middleware-in-aspnet-core" type="image/png" length="0"/>
    </item>

    <item>
      <title><![CDATA[Clustered vs Non Clustered Database Indexes]]></title>
      <description><![CDATA[Database indexing with comprehensive guide on clustered and non-clustered indexes. Learn B-Tree architecture. ]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/difference-between-cluster-and-non-cluster-index" alt="Clustered vs Non Clustered Database Indexes" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>Clustered and Non-Clustered Implementations</h1>
<h2>Introduction to Database Indexing</h2>
<p>Database indexes help the database locate data faster by reducing the amount of data that must be read from disk.</p>
<p>Without an index, the database engine performs a <strong>table scan</strong>, which means reading every data page and evaluating each row until the query condition is satisfied. This approach is acceptable for very small tables but quickly becomes a bottleneck as data volume grows.</p>
<p>Indexes exist to avoid this cost by allowing the engine to jump directly to relevant pages.</p>
<p>Indexes improve:</p>
<ul><li>Query performance by reducing I/O</li>
<li>Sorting efficiency by maintaining ordered structures</li>
<li>Join performance by enabling fast lookups</li>
<li>CPU usage by minimizing row evaluations</li>
<li>Overall system throughput under concurrent load</li>
</ul>
Internally, an index is a data structure that maps <strong>key values to row locations</strong>. The database optimizer decides whether to use an index based on cost estimation, not based on whether the index exists.
<p>Creating the right index is not about indexing everything. It is about indexing access patterns.</p>
<p>---</p>
<h2>How Tables Are Organized</h2>
<p>!<a href="/blog/cluster-index/clustered-vs-nonclustered-overview.png">Clustered vs Non-Clustered Index Overview</a></p>
<p>A table can be organized in two fundamental ways:</p>
<ul><li>Physically ordered by a clustered index</li>
<li>Unordered as a heap with optional non-clustered indexes</li>
</ul>
Understanding how each option affects storage, reads, and writes is critical for designing scalable schemas.
<p>---</p>
<h2>B-Tree Architecture: How Indexes Work</h2>
<p>Most relational databases use <strong>B-Tree structures</strong> for indexes. This includes SQL Server, PostgreSQL, MySQL (InnoDB), and Oracle.</p>
<p>A B-Tree keeps data balanced and shallow, which guarantees predictable performance even as tables grow to millions or billions of rows.</p>
<p>Key properties:</p>
<ul><li>All leaf nodes are at the same depth</li>
<li>The tree remains balanced automatically</li>
<li>Lookups require a small number of page reads</li>
</ul>
!<a href="/blog/cluster-index/btree-structure.jpg">B-Tree Structure Visualization</a>
<h3>How a B-Tree Works</h3>
<ul><li>The root node contains key ranges</li>
<li>Intermediate nodes narrow the search range</li>
<li>Leaf nodes contain either data rows or row locators</li>
<li>Each page stores multiple keys to reduce tree height</li>
</ul>
Because of this design, index operations scale logarithmically with data size.
<h3>Operation Complexity</h3>
<table>
  <thead>
    <tr>
      <th>Operation</th>
      <th>Complexity</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Equality Seek</td>
      <td>O(log n)</td>
      <td>Direct lookup by key</td>
    </tr>
    <tr>
      <td>Range Query</td>
      <td>O(log n + k)</td>
      <td>k = number of rows returned</td>
    </tr>
    <tr>
      <td>Insert</td>
      <td>O(log n)</td>
      <td>May cause page split</td>
    </tr>
    <tr>
      <td>Delete</td>
      <td>O(log n)</td>
      <td>May cause page merge</td>
    </tr>
  </tbody>
</table>
<p>In practice, most index seeks require only 2 to 4 page reads, even for very large tables.</p>
<p>---</p>
<h2>Clustered Indexes</h2>
<p>A <strong>clustered index defines the physical order of rows on disk</strong>.</p>
<p>When a table has a clustered index, the leaf level of the B-Tree <strong>is the table itself</strong>. There is no separate data structure for the table rows.</p>
<p>Because data can only be physically ordered one way, a table can have only one clustered index.</p>
<p>!<a href="/blog/cluster-index/clustered-index-structure.jpg">Clustered Index Structure</a></p>
<h3>Key Characteristics</h3>
<ul><li>Data pages are sorted by the clustered key</li>
<li>Non-unique keys are made unique using an internal uniquifier</li>
<li>Range queries are extremely efficient</li>
<li>Large inserts benefit from sequential keys</li>
<li>Page splits occur when inserting into the middle of the key range</li>
</ul>
<h3>Example</h3>
<pre><code class="language-sql">CREATE CLUSTERED INDEX IX_Orders_Date 
ON Orders (OrderDate);
</code></pre>
<p>This layout is ideal for queries such as:</p>
<pre><code class="language-sql">SELECT *
FROM Orders
WHERE OrderDate BETWEEN '2024-01-01' AND '2024-01-31';
</code></pre>
<h3>What Happens on Disk</h3>
<p>``<code>
Page 1023 -> rows for 2024-01-01
Page 1024 -> rows for 2024-01-02
Page 1025 -> rows for 2024-01-03
</code>`<code></p>
<p>The engine reads pages sequentially, which is optimal for disk and memory access.</p>
<h3>Choosing a Good Clustered Key</h3>
<p>A good clustered key should be:</p>
<ul><li>Narrow (few bytes)</li>
<li>Immutable (rarely updated)</li>
<li>Sequential when possible</li>
<li>Frequently used in range queries</li>
</ul>
Bad clustered keys include GUIDs generated randomly and frequently updated columns.
<h3>Fill Factor</h3>
<p>Fill factor controls how much free space is left on index pages during creation or rebuild.</p>
<pre><code class="language-sql">CREATE CLUSTERED INDEX IX_Customers_Cluster 
ON Customers (LastName)
WITH (FILLFACTOR = 90);
</code></pre>
<p>Lower fill factors reduce page splits at the cost of higher storage usage.</p>
<h3>Composite Clustered Index</h3>
<p>Composite keys allow finer control over ordering.</p>
<pre><code class="language-sql">CREATE CLUSTERED INDEX IX_Orders_Composite
ON Orders (OrderDate DESC, OrderID ASC);
</code></pre>
<p>This supports stable ordering when multiple rows share the same date.</p>
<p>---</p>
<h2>Non-Clustered Indexes</h2>
<p>A <strong>non-clustered index</strong> is a separate structure that stores keys and row locators.</p>
<p>Unlike clustered indexes, the leaf level does not contain full rows.</p>
<p>What it stores depends on the table type:</p>
<ul><li>Heap: row identifier (RID)</li>
<li>Clustered table: clustered key value</li>
</ul>
!<a href="/blog/cluster-index/nonclustered-index-structure.jpg">Non-Clustered Index Structure</a>
<h3>Example</h3>
<pre><code class="language-sql">CREATE NONCLUSTERED INDEX IX_Orders_Customer
ON Orders (CustomerID)
INCLUDE (OrderDate, TotalAmount);
</code></pre>
<p>This index supports queries that filter by </code>CustomerID<code> and select included columns without touching the base table.</p>
<h3>Storage Layout Example</h3>
</code>`<code>
Index Page:
CustomerID | Row Locator
12345      | ClusteredKey = (2024-01-02, 89123)
12346      | ClusteredKey = (2024-01-05, 89188)
</code>``
<h3>Key Lookups</h3>
<p>If a query selects columns not present in the index, the engine performs a <strong>key lookup</strong> to fetch the remaining columns from the clustered index.</p>
<p>Covering indexes eliminate this cost.</p>
<p>---</p>
<h2>Specialized Index Types</h2>
<h3>Filtered Index</h3>
<p>Filtered indexes store only a subset of rows.</p>
<pre><code class="language-sql">CREATE NONCLUSTERED INDEX IX_Users_Active
ON Users (LastLoginDate)
WHERE IsActive = 1;
</code></pre>
<p>Benefits:</p>
<ul><li>Smaller size</li>
<li>Faster seeks</li>
<li>Lower maintenance cost</li>
</ul>
Best used when predicates are stable and selective.
<h3>Columnstore Index</h3>
<p>Columnstore indexes store data by column instead of by row.</p>
<pre><code class="language-sql">CREATE COLUMNSTORE INDEX IX_Sales_Columnstore
ON Sales (ProductID, SaleDate, Quantity, Amount);
</code></pre>
<p>They are optimized for:</p>
<ul><li>Aggregations</li>
<li>Scans over large datasets</li>
<li>Analytics and reporting workloads</li>
</ul>
They are not ideal for heavy OLTP updates.
<p>---</p>
<h2>Index Selection Guide</h2>
<table>
  <thead>
    <tr>
      <th>Scenario</th>
      <th>Recommended Index</th>
      <th>Reason</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Primary Key</td>
      <td>Clustered</td>
      <td>Natural ordering</td>
    </tr>
    <tr>
      <td>Range Queries</td>
      <td>Clustered</td>
      <td>Sequential reads</td>
    </tr>
    <tr>
      <td>Frequent Lookups</td>
      <td>Non-Clustered</td>
      <td>Fast seeks</td>
    </tr>
    <tr>
      <td>Covering Queries</td>
      <td>Non-Clustered + INCLUDE</td>
      <td>Avoids lookups</td>
    </tr>
    <tr>
      <td>Low Cardinality Filters</td>
      <td>Filtered Index</td>
      <td>Smaller footprint</td>
    </tr>
    <tr>
      <td>Analytics</td>
      <td>Columnstore</td>
      <td>Compression and scans</td>
    </tr>
  </tbody>
</table>
<p>---</p>
<h2>Index Maintenance</h2>
<p>Over time, inserts and deletes cause fragmentation.</p>
<p>Fragmentation increases:</p>
<ul><li>Page reads</li>
<li>Memory usage</li>
<li>CPU overhead</li>
</ul>
<h3>Maintenance Strategy</h3>
<ul><li>Reorganize when fragmentation is between 5 and 30 percent</li>
<li>Rebuild when fragmentation exceeds 30 percent</li>
</ul>
<h3>Smart Maintenance Script</h3>
<pre><code class="language-sql">DECLARE @IndexName NVARCHAR(255), @Fragmentation FLOAT
<p>DECLARE IndexCursor CURSOR FOR
SELECT name, avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) ps
JOIN sys.indexes i 
  ON ps.object_id = i.object_id AND ps.index_id = i.index_id
WHERE ps.avg_fragmentation_in_percent > 5</p>
<p>OPEN IndexCursor
FETCH NEXT FROM IndexCursor INTO @IndexName, @Fragmentation</p>
<p>WHILE @@FETCH_STATUS = 0
BEGIN
    IF @Fragmentation > 30
        EXEC('ALTER INDEX ' + @IndexName + ' ON Orders REBUILD')
    ELSE
        EXEC('ALTER INDEX ' + @IndexName + ' ON Orders REORGANIZE')</p>
<p>FETCH NEXT FROM IndexCursor INTO @IndexName, @Fragmentation
END</p>
<p>CLOSE IndexCursor
DEALLOCATE IndexCursor
</code></pre></p>
<p>---</p>
<h2>Performance Comparison (10M Rows)</h2>
<table>
  <thead>
    <tr>
      <th>Operation</th>
      <th>Clustered</th>
      <th>Non-Clustered</th>
      <th>Heap</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Primary Key Seek</td>
      <td>0.003 ms</td>
      <td>0.003 ms + lookup</td>
      <td>2.1 ms</td>
    </tr>
    <tr>
      <td>Range Query (10k rows)</td>
      <td>12 ms</td>
      <td>45 ms</td>
      <td>1200 ms</td>
    </tr>
    <tr>
      <td>INSERT (sequential)</td>
      <td>8 ms</td>
      <td>10 ms</td>
      <td>5 ms</td>
    </tr>
    <tr>
      <td>INSERT (random)</td>
      <td>120 ms</td>
      <td>15 ms</td>
      <td>5 ms</td>
    </tr>
    <tr>
      <td>UPDATE (in-place)</td>
      <td>6 ms</td>
      <td>9 ms</td>
      <td>7 ms</td>
    </tr>
  </tbody>
</table>
<p>---</p>
<h2>Advanced Index Patterns</h2>
<h3>Indexed View</h3>
<p>Indexed views materialize aggregations.</p>
<pre><code class="language-sql">CREATE VIEW dbo.OrderSummary WITH SCHEMABINDING AS
SELECT CustomerID, COUNT_BIG(*) AS OrderCount, SUM(TotalAmount) AS Total
FROM dbo.Orders
GROUP BY CustomerID;
<p>CREATE UNIQUE CLUSTERED INDEX IX_OrderSummary
ON dbo.OrderSummary (CustomerID);
</code></pre></p>
<p>They are useful for expensive aggregations with stable data.</p>
<h3>Partitioned Index</h3>
<p>Partitioning improves manageability and query pruning.</p>
<pre><code class="language-sql">CREATE PARTITION FUNCTION OrderDateRangePF (DATE)
AS RANGE RIGHT FOR VALUES 
('2024-01-01', '2024-02-01', '2024-03-01');
</code></pre>
<p>Often combined with sliding window strategies.</p>
<p>---</p>
<h2>Troubleshooting Index Problems</h2>
<p>Useful diagnostics:</p>
<pre><code class="language-sql">SET STATISTICS XML ON;
<p>SELECT * FROM sys.dm_db_missing_index_details;</p>
<p>SELECT * FROM sys.dm_db_index_usage_stats;
</code></pre></p>
<p>Never create indexes blindly based only on missing index suggestions.</p>
<p>---</p>
<h2>Conclusion</h2>
<p>A strong indexing strategy balances:</p>
<ul><li>Read performance</li>
<li>Write cost</li>
<li>Storage usage</li>
<li>Maintenance overhead</li>
</ul>
Practical rules:
<ul><li>Choose clustered keys deliberately</li>
<li>Use non-clustered indexes to support query patterns</li>
<li>Cover critical queries with INCLUDE</li>
<li>Remove unused indexes regularly</li>
<li>Maintain indexes based on fragmentation metrics</li>
</ul>
Indexes are not optional optimizations. They are a core part of database design.
          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> November 10, 2023<br/>
            <strong>Reading Time:</strong> 8 min read<br/>
            <strong>Tags:</strong> database, sql-server, indexing, performance, b-tree, database-optimization<br/>
            <a href="https://www.modev.me/blog/difference-between-cluster-and-non-cluster-index" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/difference-between-cluster-and-non-cluster-index</link>
      <guid isPermaLink="true">https://www.modev.me/blog/difference-between-cluster-and-non-cluster-index</guid>
      <pubDate>Fri, 10 Nov 2023 10:00:00 GMT</pubDate>
      <lastBuildDate>Fri, 10 Nov 2023 10:00:00 GMT</lastBuildDate>
      <category>database</category>
      <category>sql-server</category>
      <category>indexing</category>
      <category>performance</category>
      <category>b-tree</category>
      <category>database-optimization</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/difference-between-cluster-and-non-cluster-index" type="image/png" length="0"/>
    </item>

    <item>
      <title><![CDATA[C# Boxing and Unboxing]]></title>
      <description><![CDATA[A practical guide to boxing and unboxing in C# and their performance impact.]]></description>
      <content:encoded><![CDATA[
        <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333;">
          <img src="https://www.modev.me/og/boxing-and-unboxing-in-csharp" alt="C# Boxing and Unboxing" style="max-width: 100%; height: auto; margin-bottom: 2rem; border-radius: 8px;" />
          

<h1>Deep Dive into C# Boxing and Unboxing</h1>
<h2>Understanding the Fundamentals</h2>
<p>Boxing and unboxing describe how the .NET runtime moves data between <strong>value types</strong> and <strong>reference types</strong>.</p>
<p>This behavior is rooted in how the CLR represents data in memory and how the JIT compiler generates machine code.</p>
<p>!<a href="/blog/BoxingUnboxing/csharp-boxing-unboxing-overview.png">C# Boxing and Unboxing Overview</a></p>
<h3>Value Types vs Reference Types</h3>
<strong>Value types</strong>
<ul><li><code>int</code>, <code>double</code>, <code>bool</code></li>
<li><code>struct</code>, <code>enum</code></li>
<li>Stored inline</li>
<li>Copied by value</li>
<li>Designed for small, immutable data</li>
</ul>
<strong>Reference types</strong>
<ul><li><code>class</code>, <code>string</code>, <code>object</code></li>
<li>Stored on the managed heap</li>
<li>Passed by reference</li>
<li>Require garbage collection</li>
</ul>
<h3>What Is Boxing</h3>
<strong>Boxing</strong> is the process of wrapping a value type inside a reference type (<code>object</code> or interface).
<pre><code class="language-csharp">int number = 42;
object boxed = number;
</code></pre>
<p>At runtime, this causes the CLR to allocate a new object on the managed heap and copy the value into it.</p>
<h3>What Is Unboxing</h3>
<strong>Unboxing</strong> extracts the value type from the boxed object.
<pre><code class="language-csharp">object boxed = 42;
int unboxed = (int)boxed;
</code></pre>
<p>Unboxing is not just a cast. It includes a runtime type check and a memory copy.</p>
<p>---</p>
<h2>What Actually Happens During Boxing</h2>
<pre><code class="language-csharp">int number = 42;
object boxed = number;
</code></pre>
<p>Internally, the CLR performs the following steps:</p>
<ul><li>Allocates memory on the managed heap</li>
<li>Writes an object header (method table pointer, sync block)</li>
<li>Copies the value into the object payload</li>
<li>Returns a reference to the object</li>
</ul>
Although <code>int</code> is only 4 bytes, the boxed object typically occupies <strong>24 bytes or more</strong>, depending on platform and alignment.
<h3>Why Size Increases</h3>
<p>A boxed value includes:</p>
<ul><li>Object header</li>
<li>Type metadata pointer</li>
<li>Padding for alignment</li>
<li>The actual value</li>
</ul>
This explains why boxing can dramatically increase memory usage in tight loops or large collections.
<p>---</p>
<h2>What Actually Happens During Unboxing</h2>
<pre><code class="language-csharp">object boxed = 42;
int value = (int)boxed;
</code></pre>
<p>The CLR performs:</p>
<ul><li>A runtime type check to ensure the object contains the expected value type</li>
<li>Copies the value from the heap back to the stack or register</li>
<li>Leaves the boxed object on the heap for later garbage collection</li>
</ul>
Unboxing does not free memory. The boxed object remains until collected by the GC.
<p>If the runtime type does not match, an <code>InvalidCastException</code> is thrown.</p>
<p>---</p>
<h2>Memory Management</h2>
<p>!<a href="/blog/BoxingUnboxing/stack-vs-heap-dotnet.webp">Stack vs Heap in .NET</a></p>
<h3>Stack vs Heap in Practice</h3>
<strong>Stack</strong>
<ul><li>Extremely fast allocation</li>
<li>Automatic cleanup</li>
<li>Limited size</li>
<li>Used for local value types and method frames</li>
</ul>
<strong>Heap</strong>
<ul><li>Slower allocation</li>
<li>Managed by garbage collector</li>
<li>Larger and flexible</li>
<li>Used for reference types and boxed values</li>
</ul>
<h3>Example Memory Layout</h3>
<pre><code class="language-csharp">int x = 10;
object boxed = x;
</code></pre>
<ul><li><code>x</code> lives inline in the stack frame</li>
<li><code>boxed</code> is a reference pointing to heap memory</li>
<li>The value <code>10</code> is duplicated, not shared</li>
</ul>
This duplication is a key reason boxing should be avoided in performance-sensitive paths.
<p>---</p>
<h2>Why Boxing Hurts Performance</h2>
<p>Boxing introduces multiple hidden costs:</p>
<ul><li>Heap allocation</li>
<li>Additional memory usage</li>
<li>CPU cycles for copying</li>
<li>Garbage collection pressure</li>
<li>Cache inefficiency</li>
</ul>
Unboxing adds:
<ul><li>Runtime type checking</li>
<li>Additional memory copy</li>
</ul>
These costs are often invisible in small programs but become severe in:
<ul><li>Hot loops</li>
<li>High-throughput services</li>
<li>Real-time systems</li>
<li>Large collections</li>
</ul>
---
<h2>Performance Analysis</h2>
<p>!<a href="/blog/BoxingUnboxing/boxing-performance-allocation.png">Boxing Performance and Memory Allocation</a></p>
<h3>Boxing in Collections</h3>
<p>Compare the same loop with an object-based collection and a generic collection:</p>
<Tabs label="Collection implementations" labels={['ArrayList (boxing)', 'List<int> (no boxing)']}>
<Tab>
<p>``<code>csharp title="ArrayListExample.cs" {4} showLineNumbers
ArrayList list = new ArrayList();
for (int i = 0; i < 1_000_000; i++)
{
    list.Add(i);
}
</code>`<code></p>
<p>Each </code>Add<code> call boxes </code>int<code> into </code>object<code>.</p>
</Tab>
<Tab>
</code>`<code>csharp title="GenericListExample.cs" {1,4} showLineNumbers
List<int> list = new List<int>();
for (int i = 0; i < 1_000_000; i++)
{
    list.Add(i);
}
</code>`<code>
<p>The generic version:</p>
<ul><li>Avoids boxing entirely</li>
<li>Uses contiguous memory</li>
<li>Is easier for the JIT to optimize</li>
</ul>
</Tab>
</Tabs>
<h3>Memory Footprint Comparison</h3>
<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Unboxed Size</th>
      <th>Boxed Size</th>
      <th>Approx Increase</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>bool</td>
      <td>1 byte</td>
      <td>~24 bytes</td>
      <td>24x</td>
    </tr>
    <tr>
      <td>int</td>
      <td>4 bytes</td>
      <td>~24 bytes</td>
      <td>6x</td>
    </tr>
    <tr>
      <td>long</td>
      <td>8 bytes</td>
      <td>~24 bytes</td>
      <td>3x</td>
    </tr>
    <tr>
      <td>decimal</td>
      <td>16 bytes</td>
      <td>~40 bytes</td>
      <td>2.5x</td>
    </tr>
  </tbody>
</table>
<p>These numbers explain why legacy non-generic APIs scale poorly.</p>
<p>---</p>
<h2>Common Boxing Scenarios</h2>
<h3>Explicit Boxing</h3>
<pre><code class="language-csharp">object o = 10;
</code></pre>
<h3>Implicit Boxing</h3>
<pre><code class="language-csharp">Console.WriteLine(10);
</code></pre>
<p>The </code>WriteLine(object)<code> overload causes boxing.</p>
<h3>Interface Boxing</h3>
<pre><code class="language-csharp">IComparable c = 10;
</code></pre>
<p>Value types implementing interfaces are boxed unless constrained generically.</p>
<h3>Enum Boxing</h3>
<pre><code class="language-csharp">Enum e = DayOfWeek.Monday;
</code></pre>
<p>Enums are value types and get boxed when treated as </code>Enum<code> or </code>object<code>.</p>
<p>---</p>
<h2>String Interpolation and Boxing</h2>
<pre><code class="language-csharp">int value = 42;
string s = $"Value is {value}";
</code></pre>
<p>This may cause boxing depending on overload resolution.</p>
<p>Safer alternative:</p>
<pre><code class="language-csharp">string s = $"Value is {value:D}";
</code></pre>
<p>Or:</p>
<pre><code class="language-csharp">string s = string.Create(
    CultureInfo.InvariantCulture,
    $"Value is {value}"
);
</code></pre>
<p>In high-frequency logging, prefer structured logging frameworks that avoid boxing.</p>
<p>---</p>
<h2>Performance Optimization Example</h2>
<pre><code class="language-csharp">const int count = 1_000_000;
<p>// No boxing
int sum1 = 0;
for (int i = 0; i < count; i++)
{
    sum1 += i;
}</p>
<p>// Boxing
object sum2 = 0;
for (int i = 0; i < count; i++)
{
    sum2 = (int)sum2 + i;
}
</code></pre></p>
<p>The second loop:</p>
<ul><li>Boxes on every iteration</li>
<li>Allocates millions of objects</li>
<li>Triggers frequent GC cycles</li>
</ul>
This pattern is a common hidden performance bug.
<p>---</p>
<h2>Best Practices</h2>
<h3>Recommended</h3>
<ul><li>Prefer generics everywhere</li>
<li>Use </code>List<T><code>, </code>Dictionary<TKey,TValue><code></li>
<li>Implement </code>IEquatable<T><code> on structs</li>
<li>Keep structs small and immutable</li>
<li>Use profilers to detect boxing</li>
</ul>
<h3>Avoid</h3>
<ul><li></code>ArrayList<code>, </code>Hashtable<code></li>
<li>APIs that accept </code>object<code> unnecessarily</li>
<li>Structs implementing non-generic interfaces</li>
<li>Passing value types through </code>object<code> pipelines</li>
</ul>
---
<h2>Advanced Scenarios</h2>
<h3>Designing Structs Correctly</h3>
<pre><code class="language-csharp">public readonly struct Money : IEquatable<Money>
{
    private readonly decimal amount;
<p>public Money(decimal amount)
    {
        this.amount = amount;
    }</p>
<p>public bool Equals(Money other) => amount == other.amount;</p>
<p>public override bool Equals(object obj)
    {
        return obj is Money other && Equals(other);
    }</p>
<p>public override int GetHashCode() => amount.GetHashCode();
}
</code></pre></p>
<p>Implementing </code>IEquatable<T><code> avoids boxing during equality checks in generic collections.</p>
<h3>Generic Constraints to Prevent Boxing</h3>
<pre><code class="language-csharp">public class Processor<T> where T : struct
{
    public T Process(T value)
    {
        return value;
    }
}
</code></pre>
<p>The </code>struct<code> constraint allows the JIT to generate boxing-free code paths.</p>
<p>---</p>
<h2>Modern C# Features That Reduce Boxing</h2>
<ul><li>Generics</li>
<li></code>Span<T><code> and </code>ReadOnlySpan<T><code></li>
<li>Nullable value types (</code>int?<code>)</li>
<li>ValueTask</li>
<li>Pattern matching with generics</li>
</ul>
When used correctly, modern C# allows writing allocation-free code in most scenarios.
<p>---</p>
<h2>Summary</h2>
<p>Boxing and unboxing are fundamental CLR behaviors that directly impact performance and memory usage.</p>
<p>They are acceptable in:</p>
<ul><li>Low-frequency code</li>
<li>Application boundaries</li>
<li>Debug or tooling scenarios</li>
</ul>
They should be avoided in:
<ul><li>Hot paths</li>
<li>Tight loops</li>
<li>High-throughput services</li>
<li>Allocation-sensitive systems</li>
</ul>
Understanding boxing is not optional for performance-critical .NET development. It is a core part of writing efficient, scalable C# code.
<h2>Verified Examples</h2>
<p>Checked on <strong>September 16, 2026</strong> with <strong>.NET 10.0.12</strong> using SDK 10.0.112. The functional checks cover copying a value into a box, unboxing it to the exact stored type, rejecting an invalid cast, and preserving the same values in </code>ArrayList<code> and </code>List<int><code>.</p>
<p>The verification harness is </code>scripts/verify-blog/dotnet/Program.cs` in the portfolio repository. These checks exercise behavior; allocation sizes and performance measurements require their own workload and runtime configuration.</p>
          <hr style="margin: 2rem 0; border: none; border-top: 1px solid #e0e0e0;" />
          <p style="font-size: 0.9rem; color: #666;">
            <strong>Author:</strong> Mohammed Mostafa<br/>
            <strong>Published:</strong> August 22, 2023<br/>
            <strong>Reading Time:</strong> 7 min read<br/>
            <strong>Tags:</strong> csharp, dotnet, performance, memory-management, programming-fundamentals<br/>
            <a href="https://www.modev.me/blog/boxing-and-unboxing-in-csharp" style="color: #0066cc; text-decoration: none;">Read on modev.me →</a>
          </p>
        </div>
      ]]></content:encoded>
      <link>https://www.modev.me/blog/boxing-and-unboxing-in-csharp</link>
      <guid isPermaLink="true">https://www.modev.me/blog/boxing-and-unboxing-in-csharp</guid>
      <pubDate>Tue, 22 Aug 2023 10:00:00 GMT</pubDate>
      <lastBuildDate>Wed, 16 Sep 2026 00:00:00 GMT</lastBuildDate>
      <category>csharp</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>memory-management</category>
      <category>programming-fundamentals</category>
      <author>mohammedmostafanazih@gmail.com (Mohammed Mostafa)</author>
      <enclosure url="https://www.modev.me/og/boxing-and-unboxing-in-csharp" type="image/png" length="0"/>
    </item>
  </channel>
</rss>