<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mukul S</title>
    <description>The latest articles on DEV Community by Mukul S (@mukul_sharma_61fc4dd6f9d8).</description>
    <link>https://dev.to/mukul_sharma_61fc4dd6f9d8</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3518766%2F548d981e-ab90-4068-b53c-831456d6aeec.jpg</url>
      <title>DEV Community: Mukul S</title>
      <link>https://dev.to/mukul_sharma_61fc4dd6f9d8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mukul_sharma_61fc4dd6f9d8"/>
    <language>en</language>
    <item>
      <title>The Hidden Cost of a Log Line : Sync/Async Flush and everything in Between</title>
      <dc:creator>Mukul S</dc:creator>
      <pubDate>Wed, 29 Jul 2026 06:37:15 +0000</pubDate>
      <link>https://dev.to/mukul_sharma_61fc4dd6f9d8/the-hidden-cost-of-a-log-line-syncasync-flush-and-everything-in-between-13of</link>
      <guid>https://dev.to/mukul_sharma_61fc4dd6f9d8/the-hidden-cost-of-a-log-line-syncasync-flush-and-everything-in-between-13of</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;log.info("user logged in")&lt;/code&gt; looks free. It isn't. Behind that one line is a chain of decisions — buffer or not, flush or not, block or drop, same thread or another — and each one trades &lt;strong&gt;latency&lt;/strong&gt;, &lt;strong&gt;throughput&lt;/strong&gt;, and &lt;strong&gt;durability&lt;/strong&gt; against the others. This post walks the whole chain, from the method call down to the bytes hitting the disk platter.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've ever wondered why your p99 latency has a mysterious spike, why logs vanish after a crash, or what "async logging" actually buys you, this is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the map: facade vs. implementation
&lt;/h2&gt;

&lt;p&gt;Java logging is a two-layer cake, and mixing up the layers is the #1 source of confusion.&lt;br&gt;
&lt;strong&gt;The facade&lt;/strong&gt; is the API your code calls. &lt;strong&gt;The implementation&lt;/strong&gt; is what actually writes the bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your code
   │  log.info(...)
   ▼
┌───────────────────────────────┐
│  Facade: SLF4J (or Log4j2 API)│   ← the interface you compile against
└──────────────┬────────────────┘
               │ bound at runtime
   ┌───────────┼────────────┬──────────────┐
   ▼           ▼            ▼              ▼
Logback   Log4j2 Core   java.util.logging  ...
(the engine that buffers, formats, and flushes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SLF4J&lt;/strong&gt; — the de-facto standard facade. Your app should log against this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logback&lt;/strong&gt; — the reference SLF4J implementation. Solid, widely deployed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log4j2&lt;/strong&gt; — the performance-focused implementation, famous for its lock-free async loggers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;java.util.logging (JUL)&lt;/strong&gt; — built into the JDK, rarely chosen on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the split? So you can swap engines without touching a single &lt;code&gt;log.&lt;/code&gt; call. Everything interesting in this post — the buffering, the flushing, the async magic — happens in the &lt;strong&gt;implementation&lt;/strong&gt; layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The anatomy of a single log call
&lt;/h2&gt;

&lt;p&gt;Before we talk flushing, let's see what one &lt;code&gt;log.info(...)&lt;/code&gt; actually does. There are five stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Level check      → is INFO enabled for this logger? (cheap, often the fastest bail-out)
2. Build LogEvent   → capture message, timestamp, thread, MDC context, maybe a stack trace
3. Filter           → run any configured filters
4. Layout / encode  → turn the event into bytes ("2026-07-28 12:00:01 INFO ...")
5. Append           → write those bytes to the destination (file, console, socket)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stage 5 — the &lt;strong&gt;append&lt;/strong&gt; — is where sync vs. async and flush-vs-no-flush live. It's also, by far, the most expensive stage, because it may touch the disk.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Pro tip already:&lt;/strong&gt; Stage 1 is why you sometimes see &lt;code&gt;if (log.isDebugEnabled())&lt;/code&gt;. It skips stages 2–5 when the level is off. With modern parameterized logging (&lt;code&gt;log.debug("x={}", x)&lt;/code&gt;) the framework does this check for you &lt;em&gt;before&lt;/em&gt; building the string — so &lt;code&gt;log.debug("x=" + x)&lt;/code&gt; is the real anti-pattern, because the string concatenation happens regardless.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Sync logging: the default, and its two hidden layers
&lt;/h2&gt;

&lt;p&gt;"Synchronous" means the append happens &lt;strong&gt;on your application thread&lt;/strong&gt;. Your thread doesn't return from &lt;code&gt;log.info(...)&lt;/code&gt; until the write is done. Simple, predictable — and where all the flush nuance lives.&lt;br&gt;
Here's the subtlety most people miss: between your log statement and the actual disk, there are &lt;strong&gt;two separate buffers&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; your app thread
      │  writes formatted bytes
      ▼
┌─────────────────────┐
│ App-level buffer    │   ← BufferedOutputStream inside the appender
│ (e.g. 4 KB)         │      flush() empties THIS
└──────────┬──────────┘
           │ flush()
           ▼
┌─────────────────────┐
│ OS page cache       │   ← the kernel's copy, still in RAM
│ (kernel memory)     │      fsync() empties THIS
└──────────┬──────────┘
           │ fsync()
           ▼
┌─────────────────────┐
│ Physical disk       │   ← now it survives a power loss
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different operations empty two different buffers, and people constantly confuse them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;flush()&lt;/code&gt;&lt;/strong&gt; pushes bytes from the &lt;em&gt;application&lt;/em&gt; buffer into the &lt;em&gt;OS page cache&lt;/em&gt;. After a flush, another process (like &lt;code&gt;tail -f&lt;/code&gt;) can see your log line. &lt;strong&gt;But it is still in RAM&lt;/strong&gt; — a kernel panic or power loss loses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fsync()&lt;/code&gt;&lt;/strong&gt; (via &lt;code&gt;FileChannel.force()&lt;/code&gt;) forces the OS to write the page cache to &lt;em&gt;physical storage&lt;/em&gt;. This is what actually makes a log line survive a crash — and it's dramatically slower.
Most logging frameworks give you knobs for the first buffer and, effectively, never touch the second by default. That's the durability tradeoff hiding in plain sight.
### &lt;code&gt;immediateFlush&lt;/code&gt;: the knob that matters most
Both Logback and Log4j2 file appenders expose &lt;code&gt;immediateFlush&lt;/code&gt;:
&lt;strong&gt;Logback:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;appender&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"FILE"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ch.qos.logback.core.FileAppender"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;file&amp;gt;&lt;/span&gt;app.log&lt;span class="nt"&gt;&amp;lt;/file&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;immediateFlush&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/immediateFlush&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- default: true --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;encoder&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;pattern&amp;gt;&lt;/span&gt;%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n&lt;span class="nt"&gt;&amp;lt;/pattern&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/encoder&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/appender&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Log4j2:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;File&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"FILE"&lt;/span&gt; &lt;span class="na"&gt;fileName=&lt;/span&gt;&lt;span class="s"&gt;"app.log"&lt;/span&gt; &lt;span class="na"&gt;immediateFlush=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PatternLayout&lt;/span&gt; &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/File&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;immediateFlush=true&lt;/code&gt;&lt;/strong&gt; (the default): call &lt;code&gt;flush()&lt;/code&gt; after &lt;strong&gt;every&lt;/strong&gt; log event. Your line is in the OS cache the instant the call returns. Safe if the JVM crashes (the OS still has the bytes and will write them). &lt;strong&gt;But&lt;/strong&gt; it means a syscall per log line — the throughput killer under load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;immediateFlush=false&lt;/code&gt;&lt;/strong&gt;: let the &lt;code&gt;BufferedOutputStream&lt;/code&gt; fill up (typically 8 KB) and flush only when it's full. Far fewer syscalls, much higher throughput. The cost: if the JVM dies, whatever's sitting in the app buffer (up to 8 KB of your most recent, most interesting logs) is &lt;strong&gt;gone&lt;/strong&gt;.
That's the fundamental sync-flush tradeoff in one sentence: &lt;strong&gt;flush every line for safety, or buffer for speed.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: Log4j2 automatically forces &lt;code&gt;immediateFlush=false&lt;/code&gt; behavior for its &lt;strong&gt;async&lt;/strong&gt; loggers — because in the async world, the fix for durability is different (more on that below).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  So does sync logging survive a crash?
&lt;/h3&gt;

&lt;p&gt;Depends on &lt;em&gt;which&lt;/em&gt; crash and &lt;em&gt;which&lt;/em&gt; flush setting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;&lt;code&gt;immediateFlush=true&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;immediateFlush=false&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;With &lt;code&gt;fsync&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JVM crash (exception, OOM)&lt;/td&gt;
&lt;td&gt;✅ safe (OS has it)&lt;/td&gt;
&lt;td&gt;⚠️ lose app buffer&lt;/td&gt;
&lt;td&gt;✅ safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kill -9 the process&lt;/td&gt;
&lt;td&gt;✅ safe (OS has it)&lt;/td&gt;
&lt;td&gt;⚠️ lose app buffer&lt;/td&gt;
&lt;td&gt;✅ safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS kernel panic&lt;/td&gt;
&lt;td&gt;❌ lose page cache&lt;/td&gt;
&lt;td&gt;❌ lose more&lt;/td&gt;
&lt;td&gt;✅ safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power loss&lt;/td&gt;
&lt;td&gt;❌ lose page cache&lt;/td&gt;
&lt;td&gt;❌ lose more&lt;/td&gt;
&lt;td&gt;✅ safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Almost nobody does &lt;code&gt;fsync&lt;/code&gt; per log line — it's punishingly slow (milliseconds per call). Logs are treated as "best effort durable," and that's usually the right call. Just know the guarantee you're actually getting.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Async logging: get off the hot path
&lt;/h2&gt;

&lt;p&gt;Sync logging's real problem isn't correctness — it's that &lt;strong&gt;your request thread pays the I/O bill&lt;/strong&gt;. If the disk hiccups, if a log rotation stalls, if the buffer flushes at the wrong moment, that latency lands directly on the user request that happened to log at that instant. This is a classic source of mysterious p99 spikes.&lt;br&gt;
Async logging fixes this by handing the log event to &lt;strong&gt;another thread&lt;/strong&gt; and returning immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; app thread                         background thread
     │  log.info(...)                     │
     │──── enqueue event ────►  [ queue ] │
     │  returns instantly                 │──► format + write + flush
     ▼                                    ▼
 keep serving the request         does the slow I/O
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app thread's job shrinks to "drop the event in a queue and move on." The slow work — formatting, writing, flushing — happens on a dedicated logging thread that nobody's waiting on.&lt;br&gt;
There are two very different implementations of this idea, and the difference is the whole ballgame.&lt;/p&gt;
&lt;h3&gt;
  
  
  Flavor 1: Logback / Log4j2 &lt;code&gt;AsyncAppender&lt;/code&gt; (a blocking queue)
&lt;/h3&gt;

&lt;p&gt;This is the classic approach: wrap a real appender in an async one backed by a &lt;code&gt;BlockingQueue&lt;/code&gt; (an &lt;code&gt;ArrayBlockingQueue&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Logback --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;appender&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ASYNC"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ch.qos.logback.classic.AsyncAppender"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;queueSize&amp;gt;&lt;/span&gt;256&lt;span class="nt"&gt;&amp;lt;/queueSize&amp;gt;&lt;/span&gt;              &lt;span class="c"&gt;&amp;lt;!-- default: 256 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;discardingThreshold&amp;gt;&lt;/span&gt;51&lt;span class="nt"&gt;&amp;lt;/discardingThreshold&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- default: 20% of queueSize --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;neverBlock&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/neverBlock&amp;gt;&lt;/span&gt;          &lt;span class="c"&gt;&amp;lt;!-- default: false --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;appender-ref&lt;/span&gt; &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"FILE"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/appender&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three knobs that decide its behavior under stress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;queueSize&lt;/code&gt;&lt;/strong&gt; — how many events can wait in line. Bigger = absorbs bigger bursts, uses more memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;discardingThreshold&lt;/code&gt;&lt;/strong&gt; — when the queue gets this full, &lt;strong&gt;drop lower-priority events&lt;/strong&gt; (TRACE/DEBUG/INFO) and keep WARN/ERROR. Logback defaults this to 20% of the queue remaining. Set it to &lt;code&gt;0&lt;/code&gt; to &lt;em&gt;never&lt;/em&gt; discard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;neverBlock&lt;/code&gt;&lt;/strong&gt; — what to do when the queue is completely full:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;false&lt;/code&gt; (default): the app thread &lt;strong&gt;blocks&lt;/strong&gt; until space frees up. You keep every log, but now you've reintroduced the exact latency you were trying to avoid — async silently becomes sync under load.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt;: drop the event and move on. Latency stays flat, but you lose logs during bursts.
&lt;strong&gt;There is no free lunch here.&lt;/strong&gt; A bounded queue under sustained overload has only three options: block the producer, drop events, or grow unbounded (OOM). Every async logger is just choosing which of these to do, and when.
### Flavor 2: Log4j2 Async Loggers (the LMAX Disruptor)
Log4j2's headline feature is &lt;strong&gt;Async Loggers&lt;/strong&gt;, built on the &lt;strong&gt;LMAX Disruptor&lt;/strong&gt; — a lock-free ring buffer that came out of high-frequency trading. This is a genuinely different beast from a blocking queue, and it's why Log4j2 benchmarks so aggressively.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Make ALL loggers async (system property) --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Or mix: specific loggers async, rest sync --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;AsyncLogger&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"com.myapp"&lt;/span&gt; &lt;span class="na"&gt;level=&lt;/span&gt;&lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Root&lt;/span&gt; &lt;span class="na"&gt;level=&lt;/span&gt;&lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AppenderRef&lt;/span&gt; &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"FILE"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Root&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why a ring buffer instead of a queue? Two reasons:&lt;br&gt;
&lt;strong&gt;1. Lock-free = no contention.&lt;/strong&gt; A &lt;code&gt;BlockingQueue&lt;/code&gt; uses locks; when many threads log at once, they fight over that lock and serialize. The Disruptor uses a pre-allocated ring buffer and atomic sequence counters (CAS) instead of locks. Producers and the consumer coordinate via cursor positions, not mutual exclusion. At high thread counts this is the difference — Log4j2's own numbers show async loggers sustaining &lt;strong&gt;millions&lt;/strong&gt; of messages/sec where a blocking queue plateaus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ring buffer (pre-allocated slots, reused forever)
        ┌───┬───┬───┬───┬───┬───┬───┬───┐
        │ 5 │ 6 │ 7 │   │   │ 1 │ 2 │ 3 │
        └───┴───┴───┴─▲─┴───┴───┴───┴─▲─┘
                      │               │
              consumer cursor    producer cursor
              (writes to disk)   (app threads publish here)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Garbage-free.&lt;/strong&gt; The slots in the ring buffer are allocated once and &lt;strong&gt;reused&lt;/strong&gt;. A queue allocates a new node per event, feeding the garbage collector. The Disruptor pre-allocates, so in steady state it creates (almost) no garbage — which means no GC pauses caused by logging. Log4j2 pairs this with a "garbage-free" layout mode that reuses &lt;code&gt;StringBuilder&lt;/code&gt;s and buffers, so you can log hard with near-zero allocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when the ring buffer fills? &lt;code&gt;AsyncQueueFullPolicy&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Same fundamental problem as before — a bounded buffer can overflow — and Log4j2 makes the policy explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Default&lt;/code&gt; policy&lt;/strong&gt;: the producing thread &lt;strong&gt;blocks (busy-spins/waits)&lt;/strong&gt; until a slot frees up. No lost logs, but backpressure hits your app thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Discard&lt;/code&gt; policy&lt;/strong&gt;: drop events at or below a configured level (default: drop INFO and below) when full. Keeps latency flat, loses low-priority logs.&lt;/li&gt;
&lt;li&gt;You can also plug in a &lt;strong&gt;custom&lt;/strong&gt; policy.
And there's a sharp edge worth knowing: if a log call happens &lt;em&gt;on the background consumer thread itself&lt;/em&gt; (e.g. logging from inside a layout or an exception handler), blocking would deadlock — so Log4j2 detects this and routes it synchronously instead.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The durability twist: async can lose logs a sync setup wouldn't
&lt;/h2&gt;

&lt;p&gt;Here's the tradeoff people forget. With async logging, when your app thread returns from &lt;code&gt;log.error("about to crash")&lt;/code&gt;, that event is &lt;strong&gt;just sitting in a queue in memory&lt;/strong&gt;. It hasn't been formatted, let alone written or flushed.&lt;br&gt;
If the JVM crashes in the next millisecond, &lt;strong&gt;that error log — the one explaining the crash — is gone.&lt;/strong&gt; With synchronous &lt;code&gt;immediateFlush=true&lt;/code&gt; logging, the same line would have been in the OS cache and survived.&lt;br&gt;
This is the cruel irony of async logging: it's fastest exactly when you're logging the most, which is often right before something goes wrong.&lt;br&gt;
Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shutdown hooks / graceful drain.&lt;/strong&gt; Both frameworks try to flush the queue on orderly shutdown. This handles clean exits, not &lt;code&gt;kill -9&lt;/code&gt; or hard crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log fatal errors synchronously.&lt;/strong&gt; A common pattern: async for INFO/DEBUG, but route ERROR/FATAL through a synchronous, immediate-flush appender so the important stuff is durable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept the loss.&lt;/strong&gt; For high-volume, low-value logs (access logs, debug traces), losing the last few hundred on a hard crash is fine. Match the guarantee to the value of the log.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Putting numbers to it (and the benchmarking trap)
&lt;/h2&gt;

&lt;p&gt;Rough ordering of throughput, slowest to fastest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync + immediateFlush=true        ▓▓                      (syscall per line)
sync + immediateFlush=false       ▓▓▓▓▓▓                  (buffered)
async AsyncAppender (queue)       ▓▓▓▓▓▓▓▓▓▓
async Loggers (Disruptor)         ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But — and every honest benchmark says this — &lt;strong&gt;your mileage will vary wildly&lt;/strong&gt;, because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Peak vs. sustained throughput are different questions.&lt;/strong&gt; Async is fantastic at absorbing &lt;em&gt;bursts&lt;/em&gt;. But your disk's real write bandwidth is a hard ceiling. If you sustain more log volume than the disk can drain, the queue fills and async degrades to whatever its full-policy is (blocking or dropping). Async doesn't make your disk faster — it just decouples your app from disk &lt;em&gt;jitter&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout cost is often the real bottleneck.&lt;/strong&gt; A fancy pattern with caller location (&lt;code&gt;%class&lt;/code&gt;, &lt;code&gt;%line&lt;/code&gt;, &lt;code&gt;%method&lt;/code&gt;) forces a stack-trace walk on &lt;strong&gt;every&lt;/strong&gt; log line — that can cost more than the write itself. Avoid location info in hot paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microbenchmarks lie.&lt;/strong&gt; Logging in a tight loop with nothing else running doesn't reflect a real app where the GC, the CPU cache, and other threads are all contending.
The practical takeaway isn't a number — it's a decision tree.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  A practical decision guide
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default for most services:&lt;/strong&gt;&lt;br&gt;
Async logging (Log4j2 Async Loggers if you can, Logback AsyncAppender otherwise), with a bounded queue sized to absorb your typical burst, and low-priority events discarded under pressure.&lt;br&gt;
&lt;strong&gt;When you need every log line (audit, compliance, financial):&lt;/strong&gt;&lt;br&gt;
Synchronous + &lt;code&gt;immediateFlush=true&lt;/code&gt;. Accept the throughput hit; you're buying durability. Add &lt;code&gt;fsync&lt;/code&gt; only if you truly can't lose data on a power cut — and know it'll cost you.&lt;br&gt;
&lt;strong&gt;When latency is sacred (trading, real-time):&lt;/strong&gt;&lt;br&gt;
Log4j2 Async Loggers with garbage-free layout and a discard policy — never let logging block a request thread, and never let it trigger GC.&lt;br&gt;
&lt;strong&gt;A solid hybrid that covers most people:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO/DEBUG  → async, discard-under-pressure  (high volume, low value)
WARN/ERROR  → sync, immediateFlush=true       (low volume, high value)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get speed for the noise and durability for the signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The five things to actually remember
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A log call has two buffers below it.&lt;/strong&gt; &lt;code&gt;flush()&lt;/code&gt; empties the app buffer to the OS; &lt;code&gt;fsync()&lt;/code&gt; empties the OS to disk. They are not the same, and only the second survives a power loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;immediateFlush&lt;/code&gt; is the core sync knob.&lt;/strong&gt; &lt;code&gt;true&lt;/code&gt; = safe but a syscall per line; &lt;code&gt;false&lt;/code&gt; = fast but you lose the app buffer on a crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async moves I/O off your request thread&lt;/strong&gt; — killing latency jitter — but every bounded queue must eventually &lt;strong&gt;block, drop, or OOM&lt;/strong&gt; under sustained overload. Know which one yours does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Disruptor wins by being lock-free and garbage-free&lt;/strong&gt;, not by magic. It beats blocking queues under high thread counts and avoids GC pauses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async trades durability for speed.&lt;/strong&gt; The log explaining your crash is the one most likely to be lost in the queue. Route critical logs synchronously.
Logging feels like the most boring line in your codebase. It's also one of the few that quietly touches concurrency, the memory hierarchy, syscalls, GC, and durability all at once. Now you know what that one line is really doing. 🚀
---&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;What's your logging setup — sync-and-safe, or async-and-fast? And has "async logging ate my crash log" ever bitten you? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>performance</category>
    </item>
    <item>
      <title>NGINX, Actually Explained: Architecture, Config, and the Mental Model That Makes It Click</title>
      <dc:creator>Mukul S</dc:creator>
      <pubDate>Wed, 29 Jul 2026 06:06:37 +0000</pubDate>
      <link>https://dev.to/mukul_sharma_61fc4dd6f9d8/nginx-actually-explained-architecture-config-and-the-mental-model-that-makes-it-click-10bf</link>
      <guid>https://dev.to/mukul_sharma_61fc4dd6f9d8/nginx-actually-explained-architecture-config-and-the-mental-model-that-makes-it-click-10bf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Most people meet NGINX as a magic file they copy-paste until the 502 goes away. This post is the opposite: a tour of the &lt;em&gt;model&lt;/em&gt; behind that file, drawn straight from the official docs at &lt;a href="https://nginx.org/en/docs/" rel="noopener noreferrer"&gt;nginx.org/en/docs&lt;/a&gt;. Once the model clicks, the config stops being scary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why NGINX is fast (and why the answer matters)
&lt;/h2&gt;

&lt;p&gt;Classic web servers spawn a process or thread &lt;strong&gt;per connection&lt;/strong&gt;. That's fine at 200 connections and catastrophic at 200,000 — each thread eats memory and forces the kernel to context-switch constantly. This is the famous &lt;strong&gt;C10K problem&lt;/strong&gt;.&lt;br&gt;
NGINX was built to dodge it. Instead of "one connection = one thread," it uses a small, fixed number of &lt;strong&gt;worker processes&lt;/strong&gt;, each running a single-threaded, &lt;strong&gt;event-driven, non-blocking&lt;/strong&gt; loop. One worker juggles thousands of concurrent connections by never sitting idle waiting on I/O.&lt;/p&gt;
&lt;h2&gt;
  
  
  That one design decision explains almost everything else about NGINX. Keep it in mind as we go.
&lt;/h2&gt;
&lt;h2&gt;
  
  
  The process model: master + workers
&lt;/h2&gt;

&lt;p&gt;When NGINX starts, you get &lt;strong&gt;one master process&lt;/strong&gt; and &lt;strong&gt;several worker processes&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          ┌─────────────────┐
          │  master process │   (runs as root, reads config, binds ports)
          └───────┬─────────┘
                  │ manages
      ┌───────────┼───────────┐
      ▼           ▼           ▼
  ┌────────┐  ┌────────┐  ┌────────┐
  │worker 0│  │worker 1│  │worker 2│   (do the actual request handling)
  └────────┘  └────────┘  └────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The master process&lt;/strong&gt; does the privileged, one-time work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads and validates configuration&lt;/li&gt;
&lt;li&gt;Binds to privileged ports (like &lt;code&gt;:80&lt;/code&gt; / &lt;code&gt;:443&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Spawns, monitors, and restarts workers&lt;/li&gt;
&lt;li&gt;Handles signals for graceful reloads and upgrades
&lt;strong&gt;Worker processes&lt;/strong&gt; do everything else — accepting connections, reading requests, talking to upstreams, sending responses. They're the hot path.
A sane default is one worker per CPU core:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;worker_processes&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# matches the number of available cores&lt;/span&gt;
&lt;span class="k"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;worker_connections&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# max simultaneous connections *per worker*&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your theoretical connection ceiling is roughly &lt;code&gt;worker_processes × worker_connections&lt;/code&gt;. With 8 cores and 1024 connections each, that's ~8,000 connections without breaking a sweat — and that's a conservative default.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trick that makes reloads zero-downtime
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;nginx -s reload&lt;/code&gt; and NGINX doesn't drop a single connection. The master:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Re-reads and validates the new config&lt;/li&gt;
&lt;li&gt;Starts &lt;strong&gt;new&lt;/strong&gt; workers with the new config&lt;/li&gt;
&lt;li&gt;Tells &lt;strong&gt;old&lt;/strong&gt; workers to stop accepting new connections&lt;/li&gt;
&lt;li&gt;Old workers finish their in-flight requests, then exit
This graceful choreography is why you can ship config changes to a busy production server in the middle of the day.
---
## The event loop: how one worker serves thousands
Here's the part that trips people up. A single worker is &lt;em&gt;single-threaded&lt;/em&gt;, yet handles thousands of connections. How?
Instead of blocking on I/O, each worker asks the kernel: &lt;em&gt;"tell me when any of these connections has something ready."&lt;/em&gt; On Linux that mechanism is &lt;strong&gt;&lt;code&gt;epoll&lt;/code&gt;&lt;/strong&gt; (it's &lt;code&gt;kqueue&lt;/code&gt; on BSD/macOS). The worker then loops:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wait_for_ready_connections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// epoll_wait — the only place we "block"&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="c1"&gt;// never blocks; do a slice of work, move on&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule inside a worker is sacred: &lt;strong&gt;never block.&lt;/strong&gt; A slow disk read or a slow upstream must not freeze the whole loop, because that loop is serving thousands of other clients. When work would block, NGINX either offloads it (thread pools for disk I/O) or parks the connection and comes back when the kernel says it's ready.&lt;br&gt;
Choose the connection-processing method explicitly if you want, though NGINX auto-selects the best one for your OS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="s"&gt;epoll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;# Linux&lt;/span&gt;
    &lt;span class="kn"&gt;worker_connections&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;multi_accept&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;# accept as many new connections as possible per event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  This is the whole "why is NGINX so fast" answer in one sentence: &lt;strong&gt;it turns I/O waiting into an event notification instead of a parked thread.&lt;/strong&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How NGINX processes a request
&lt;/h2&gt;

&lt;p&gt;The docs have a dedicated page on this ("How nginx processes a request"), and it's worth internalizing. Requests are routed in two steps: &lt;strong&gt;which &lt;code&gt;server&lt;/code&gt; block&lt;/strong&gt;, then &lt;strong&gt;which &lt;code&gt;location&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: pick the &lt;code&gt;server&lt;/code&gt; block
&lt;/h3&gt;

&lt;p&gt;NGINX matches the request against &lt;code&gt;listen&lt;/code&gt; directives and the &lt;code&gt;Host&lt;/code&gt; header (via &lt;code&gt;server_name&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt; &lt;span class="s"&gt;www.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;default_server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# fallback when no server_name matches&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;444&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;# drop connections to unknown hosts&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;server_name&lt;/code&gt; matching order: exact name → leading wildcard (&lt;code&gt;*.example.com&lt;/code&gt;) → trailing wildcard (&lt;code&gt;www.example.*&lt;/code&gt;) → regex. First match wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: pick the &lt;code&gt;location&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Within the chosen server, NGINX matches the URI against &lt;code&gt;location&lt;/code&gt; blocks. The matching rules are precise and worth memorizing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/exact&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# 1. exact match — highest priority, stops search&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;^~&lt;/span&gt; &lt;span class="n"&gt;/assets/&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# 2. prefix match that *skips* regex if it wins&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# 3. case-sensitive regex (first match wins)&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(jpg|png)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# 4. case-insensitive regex&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt;             &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# 5. plain prefix — longest match wins as fallback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The priority order is: &lt;code&gt;=&lt;/code&gt; beats &lt;code&gt;^~&lt;/code&gt; beats regex (&lt;code&gt;~&lt;/code&gt;/&lt;code&gt;~*&lt;/code&gt;) beats plain prefixes. Getting a 404 or serving the wrong file is &lt;em&gt;almost always&lt;/em&gt; a &lt;code&gt;location&lt;/code&gt; precedence surprise — this table is the cure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The config blocks you'll actually use
&lt;/h2&gt;

&lt;p&gt;NGINX config is a tree of &lt;strong&gt;contexts&lt;/strong&gt;. Directives are only valid in certain contexts. Here's the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main context&lt;/span&gt;
&lt;span class="k"&gt;worker_processes&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;...&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;               &lt;span class="c1"&gt;# connection processing&lt;/span&gt;
&lt;span class="s"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                       &lt;span class="c1"&gt;# everything HTTP lives here&lt;/span&gt;
    &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;mime.types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;sendfile&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;# zero-copy file serving via the kernel&lt;/span&gt;
    &lt;span class="kn"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;...&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;# a pool of backend servers&lt;/span&gt;
    &lt;span class="s"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;...&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;           &lt;span class="c1"&gt;# a virtual host&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;...&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;               &lt;span class="c1"&gt;# raw TCP/UDP proxying (databases, gRPC, etc.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Serving static files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;static.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# try file, then dir, else 404&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/assets/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;30d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;# aggressive caching for hashed assets&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;try_files&lt;/code&gt; is the workhorse here — it's how SPAs get their "always fall back to index.html" behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# client-side routing friendly&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reverse proxy to an app server
&lt;/h3&gt;

&lt;p&gt;This is probably why you're here. NGINX sits in front of your Node/Python/Go app and forwards requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# load balancing: round-robin by default&lt;/span&gt;
    &lt;span class="c1"&gt;# least_conn;                # send to the worker with fewest active conns&lt;/span&gt;
    &lt;span class="c1"&gt;# ip_hash;                   # sticky sessions by client IP&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;# reuse upstream connections&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt;              &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt;         &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;   &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;# required for upstream keepalive&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those &lt;code&gt;proxy_set_header&lt;/code&gt; lines matter more than they look. Without them, your backend sees NGINX's IP as the client, loses the original scheme, and can generate broken redirects. This four-line block is the single most copy-pasted (and most misunderstood) snippet in NGINX land.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load balancing methods, at a glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Round robin&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(default)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Backends are roughly equal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Least connections&lt;/td&gt;
&lt;td&gt;&lt;code&gt;least_conn;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Requests have uneven duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP hash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip_hash;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You need sticky sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weighted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server ... weight=3;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backends have different capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  HTTPS in about ten lines
&lt;/h2&gt;

&lt;p&gt;The docs' "Configuring HTTPS servers" page boils down to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;     &lt;span class="n"&gt;/etc/nginx/ssl/example.com.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/ssl/example.com.key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt;       &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_ciphers&lt;/span&gt;         &lt;span class="s"&gt;HIGH:!aNULL:!MD5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_session_cache&lt;/span&gt;   &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# reuse handshakes across connections&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# Redirect all HTTP to HTTPS&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;ssl_session_cache&lt;/code&gt; is the performance sleeper: TLS handshakes are expensive, and caching sessions across connections cuts CPU noticeably on busy sites.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Rate limiting: cheap insurance
&lt;/h2&gt;

&lt;p&gt;NGINX can shield your backend from bursts and abuse before a request ever reaches your app. It uses a &lt;strong&gt;leaky bucket&lt;/strong&gt; algorithm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# define a zone: 10MB of state, 10 requests/sec per client IP&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# allow short bursts of 20&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  There's a sibling directive, &lt;code&gt;limit_conn&lt;/code&gt;, for capping &lt;em&gt;concurrent&lt;/em&gt; connections per client. Both live entirely in NGINX — no app code, no extra service.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The commands worth memorizing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;                 &lt;span class="c"&gt;# test config syntax before applying — do this ALWAYS&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload          &lt;span class="c"&gt;# graceful reload, zero dropped connections&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; quit            &lt;span class="c"&gt;# graceful shutdown (finish in-flight requests)&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; stop            &lt;span class="c"&gt;# fast shutdown (drop everything now)&lt;/span&gt;
nginx &lt;span class="nt"&gt;-T&lt;/span&gt;                 &lt;span class="c"&gt;# dump the full, resolved config (includes all `include`s)&lt;/span&gt;
nginx &lt;span class="nt"&gt;-v&lt;/span&gt;                 &lt;span class="c"&gt;# version&lt;/span&gt;
nginx &lt;span class="nt"&gt;-V&lt;/span&gt;                 &lt;span class="c"&gt;# version + build flags + configured modules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rule of thumb: &lt;strong&gt;never&lt;/strong&gt; &lt;code&gt;reload&lt;/code&gt; without running &lt;code&gt;nginx -t&lt;/code&gt; first. A syntax error caught by &lt;code&gt;-t&lt;/code&gt; is a non-event; the same error hitting &lt;code&gt;reload&lt;/code&gt; on some setups can take the server down.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Debugging: read the logs like a local
&lt;/h2&gt;

&lt;p&gt;Two logs, two jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;nginx&lt;/span&gt;
&lt;span class="s"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt; &lt;span class="s"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$remote_user&lt;/span&gt; &lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$time_local&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                    &lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="nv"&gt;$body_bytes_sent&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                    &lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="nv"&gt;$http_referer&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                    &lt;span class="s"&gt;'rt=&lt;/span&gt;&lt;span class="nv"&gt;$request_time&lt;/span&gt; &lt;span class="s"&gt;uct="&lt;/span&gt;&lt;span class="nv"&gt;$upstream_connect_time&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                    &lt;span class="s"&gt;'urt="&lt;/span&gt;&lt;span class="nv"&gt;$upstream_response_time&lt;/span&gt;&lt;span class="s"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/access.log&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;error_log&lt;/span&gt;  &lt;span class="n"&gt;/var/log/nginx/error.log&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;$request_time&lt;/code&gt; and &lt;code&gt;$upstream_response_time&lt;/code&gt; to your access log is the fastest way to answer "is it NGINX or is it the backend that's slow?" — a question you &lt;em&gt;will&lt;/em&gt; be asked during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  For deep debugging, a build with &lt;code&gt;--with-debug&lt;/code&gt; unlocks &lt;code&gt;error_log ... debug;&lt;/code&gt;, which traces the request lifecycle in detail. That's covered in the docs' "A debugging log" page.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The mental model, in five lines
&lt;/h2&gt;

&lt;p&gt;If you remember nothing else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Master configures, workers serve.&lt;/strong&gt; Privilege separation + zero-downtime reloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One worker, one event loop, thousands of connections.&lt;/strong&gt; Never block the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests route in two steps:&lt;/strong&gt; &lt;code&gt;server&lt;/code&gt; block, then &lt;code&gt;location&lt;/code&gt; — and precedence is not intuitive, so learn the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contexts nest:&lt;/strong&gt; &lt;code&gt;main → http → server → location&lt;/code&gt;. Directives are context-scoped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nginx -t&lt;/code&gt; before every &lt;code&gt;reload&lt;/code&gt;.&lt;/strong&gt; Always.
Everything in the config file is a consequence of these five ideas. Once you see the event loop behind the directives, NGINX stops being a black box and starts being the most predictable thing in your stack.
---&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where to go next in the docs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginner's Guide&lt;/strong&gt; — a hands-on first config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How nginx processes a request&lt;/strong&gt; — the routing internals in depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Guide&lt;/strong&gt; — proxying, load balancing, compression, caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module reference&lt;/strong&gt; — every directive, grouped by module (&lt;code&gt;ngx_http_core_module&lt;/code&gt;, &lt;code&gt;ngx_http_proxy_module&lt;/code&gt;, &lt;code&gt;ngx_http_upstream_module&lt;/code&gt;, …)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All at &lt;a href="https://nginx.org/en/docs/" rel="noopener noreferrer"&gt;nginx.org/en/docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this useful? Drop your gnarliest NGINX config gotcha in the comments — the &lt;code&gt;location&lt;/code&gt; precedence ones are always a good time.&lt;/em&gt; 🚀&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>devops</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
