<?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: Rost</title>
    <description>The latest articles on DEV Community by Rost (@rosgluk).</description>
    <link>https://dev.to/rosgluk</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%2F3544400%2F04dd81bf-749e-4055-971f-316c0134e76c.jpg</url>
      <title>DEV Community: Rost</title>
      <link>https://dev.to/rosgluk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rosgluk"/>
    <language>en</language>
    <item>
      <title>GFM vs CommonMark vs Pandoc Markdown: Syntax Compared</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:05:11 +0000</pubDate>
      <link>https://dev.to/rosgluk/gfm-vs-commonmark-vs-pandoc-markdown-syntax-compared-2ifn</link>
      <guid>https://dev.to/rosgluk/gfm-vs-commonmark-vs-pandoc-markdown-syntax-compared-2ifn</guid>
      <description>&lt;p&gt;Markdown looks like one language until the same file renders differently on GitHub, Hugo, Obsidian, or Pandoc. And the problem is not that Markdown is unreliable.&lt;/p&gt;

&lt;p&gt;It is that "Markdown" describes a family of related syntaxes, parsers, and platform features rather than a single universal document format. CommonMark defines a precise portable core, GitHub Flavored Markdown adds features useful for software collaboration, and Pandoc Markdown expands the language into a serious document-authoring format.&lt;/p&gt;

&lt;p&gt;Choosing between them depends on where the document must render. A README file, a Hugo blog post, and an academic paper each have different requirements. This comparison is part of the broader &lt;a href="https://www.glukhov.org/documentation-tools/" rel="noopener noreferrer"&gt;documentation tooling&lt;/a&gt; picture and covers the formal dialects, platform-specific extensions, and practical portability rules so you can pick the right syntax for your target environment. For a quick syntax reference, the &lt;a href="https://www.glukhov.org/documentation-tools/markdown/markdown-cheatsheet/" rel="noopener noreferrer"&gt;Markdown cheatsheet&lt;/a&gt; covers the essential formatting elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown Is Not One Language
&lt;/h2&gt;

&lt;p&gt;The original Markdown syntax was intentionally small and loosely specified. That made it easy to read and implement, but different parsers began interpreting ambiguous input differently.&lt;/p&gt;

&lt;p&gt;CommonMark was created to define consistent parsing rules for the fundamental Markdown structures. GitHub Flavored Markdown, usually called GFM, builds on that foundation with several widely used extensions.&lt;/p&gt;

&lt;p&gt;Pandoc Markdown takes a different approach. Instead of remaining a small web-oriented syntax, it adds document features such as citations, metadata, footnotes, definition lists, attributes, and mathematical notation.&lt;/p&gt;

&lt;p&gt;A simplified relationship looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    M[Markdown family] --&amp;gt; C[CommonMark core]
    C --&amp;gt; G[GitHub Flavored Markdown]
    C --&amp;gt; X[Other CommonMark-based renderers]
    M --&amp;gt; P[Pandoc Markdown]
    G --&amp;gt; GH[GitHub platform features]
    X --&amp;gt; H[Hugo with Goldmark]
    X --&amp;gt; GL[GitLab Flavored Markdown]
    P --&amp;gt; PDF[PDF and academic workflows]
    P --&amp;gt; DOCX[DOCX and publishing workflows]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hierarchy is useful, but it is not exact inheritance in every implementation. Each renderer can enable, disable, or add syntax independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Answer
&lt;/h2&gt;

&lt;p&gt;Use CommonMark-compatible syntax when portability matters most.&lt;/p&gt;

&lt;p&gt;Use GFM when writing README files, pull requests, issue templates, and technical documentation intended primarily for GitHub-compatible platforms.&lt;/p&gt;

&lt;p&gt;Use Pandoc Markdown when the source document must become PDF, DOCX, EPUB, LaTeX, slides, or an academic paper with citations and metadata.&lt;/p&gt;

&lt;p&gt;For a Hugo technical blog, use the CommonMark core plus the Goldmark extensions that your site explicitly enables. Do not assume every feature visible on GitHub will work merely because Hugo is described as GFM-compatible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Opinionated take:&lt;/strong&gt; if you only remember one rule for a Hugo technical blog, treat CommonMark plus GFM-style tables and task lists as the default, and treat everything else — footnotes, math, callouts, header attributes — as an explicit, tested extension rather than an assumed default. That single habit prevents most of the portability failures described below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  CommonMark: The Portable Core
&lt;/h2&gt;

&lt;p&gt;CommonMark is a formal specification for the basic Markdown language. Its main contribution is not a large collection of features, but consistent parsing.&lt;/p&gt;

&lt;p&gt;It defines how parsers should interpret:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paragraphs&lt;/li&gt;
&lt;li&gt;ATX and Setext headings&lt;/li&gt;
&lt;li&gt;Block quotes&lt;/li&gt;
&lt;li&gt;Ordered and unordered lists&lt;/li&gt;
&lt;li&gt;Fenced and indented code blocks&lt;/li&gt;
&lt;li&gt;Emphasis and strong emphasis&lt;/li&gt;
&lt;li&gt;Links and images&lt;/li&gt;
&lt;li&gt;Reference-style links&lt;/li&gt;
&lt;li&gt;Inline code&lt;/li&gt;
&lt;li&gt;Thematic breaks&lt;/li&gt;
&lt;li&gt;Raw HTML blocks&lt;/li&gt;
&lt;li&gt;Hard and soft line breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A CommonMark document can still behave differently at the presentation layer. CSS, syntax highlighting, heading anchors, HTML sanitization, and link policies are outside the core parsing rules.&lt;/p&gt;

&lt;p&gt;CommonMark should therefore be treated as a reliable structural baseline, not a promise that every renderer will produce an identical page.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Portable CommonMark Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Service Deployment&lt;/span&gt;

The service exposes a small HTTP API.

&lt;span class="gu"&gt;## Requirements&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Linux
&lt;span class="p"&gt;-&lt;/span&gt; Docker
&lt;span class="p"&gt;-&lt;/span&gt; 8 GB of memory

&lt;span class="gu"&gt;## Start the service&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

See the &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;configuration guide&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;configuration.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; for details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This type of document works across almost every modern Markdown environment. It uses headings, paragraphs, lists, fenced code, and ordinary links without relying on dialect-specific extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Flavored Markdown: CommonMark for Software Projects
&lt;/h2&gt;

&lt;p&gt;GitHub Flavored Markdown is a formal dialect based on CommonMark. It preserves the CommonMark parsing model and adds features commonly needed in repository documentation and collaboration.&lt;/p&gt;

&lt;p&gt;The formal GFM specification adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pipe tables&lt;/li&gt;
&lt;li&gt;Task list items&lt;/li&gt;
&lt;li&gt;Strikethrough&lt;/li&gt;
&lt;li&gt;Extended autolinks&lt;/li&gt;
&lt;li&gt;Restrictions around some raw HTML tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These extensions are now so common that many users think they are part of standard Markdown. They are not part of the CommonMark core.&lt;/p&gt;

&lt;h3&gt;
  
  
  GFM Tables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Backend | Best use |
|---|---|
| Ollama | Local experiments |
| vLLM | Shared inference |
| SGLang | Structured workloads |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strict CommonMark parser is allowed to treat this as ordinary paragraph text. A GFM-compatible parser recognizes it as a table. For a deeper look at table syntax and alignment options, see &lt;a href="https://www.glukhov.org/documentation-tools/markdown/tables-in-markdown/" rel="noopener noreferrer"&gt;Tables in Markdown&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GFM Task Lists
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [x] Install Docker
&lt;span class="p"&gt;-&lt;/span&gt; [x] Download the model
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Add monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Task list syntax is useful in issues, pull requests, and project documentation. Outside a supporting renderer, it may appear as an ordinary list containing literal square brackets.&lt;/p&gt;

&lt;h3&gt;
  
  
  GFM Strikethrough
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Use the ~~old endpoint~~ new endpoint.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strikethrough is widely supported, but it is still an extension rather than portable CommonMark syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  GFM Autolinks
&lt;/h3&gt;

&lt;p&gt;GFM recognizes more URL-like and email-like text without requiring angle brackets or explicit link syntax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Visit https://example.com/docs for details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In strict CommonMark, explicit autolinks use angle brackets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nv"&gt;&amp;lt;https://example.com/docs&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit form is safer when a document must travel through unknown Markdown processors.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub.com Supports More Than Formal GFM
&lt;/h2&gt;

&lt;p&gt;A frequent source of confusion is the assumption that every Markdown feature visible on GitHub belongs to the GFM specification.&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;GitHub.com adds platform-level processing and features around the GFM parser. Depending on the context, GitHub can support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mathematical expressions&lt;/li&gt;
&lt;li&gt;Mermaid diagrams&lt;/li&gt;
&lt;li&gt;Alerts&lt;/li&gt;
&lt;li&gt;Issue and pull request references&lt;/li&gt;
&lt;li&gt;User and team mentions&lt;/li&gt;
&lt;li&gt;Commit references&lt;/li&gt;
&lt;li&gt;Emoji shortcodes&lt;/li&gt;
&lt;li&gt;Collapsible HTML sections&lt;/li&gt;
&lt;li&gt;Color previews&lt;/li&gt;
&lt;li&gt;Repository-relative links&lt;/li&gt;
&lt;li&gt;Automatic heading anchors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these features are syntax extensions. Others are post-processing behavior or integrations with GitHub data.&lt;/p&gt;

&lt;p&gt;This distinction matters because another renderer may accurately claim GFM compatibility without implementing GitHub's math renderer, Mermaid integration, issue references, or alert styling.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Mermaid Diagrams
&lt;/h3&gt;

&lt;p&gt;GitHub renders a fenced code block marked &lt;code&gt;mermaid&lt;/code&gt; as a diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;mermaid
&lt;/span&gt;&lt;span class="sb"&gt;flowchart LR
    A[Markdown] --&amp;gt; B[Rendered diagram]&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A generic GFM renderer may display the same block as highlighted source code. The Markdown remains valid, but the enhanced rendering is platform-specific. For a practical introduction to Mermaid syntax, see the &lt;a href="https://www.glukhov.org/documentation-tools/diagrams/mermaid-diagrams-quickstart-cheatsheet/" rel="noopener noreferrer"&gt;Mermaid Diagrams Quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Mathematical Expressions
&lt;/h3&gt;

&lt;p&gt;GitHub supports inline and block mathematical expressions using dollar delimiters and additional escaping forms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;The cache size is approximately $2nlhd$ bytes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;$$
C = 2nlhd
$$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Math is not part of formal GFM. Moving this content to another renderer requires a compatible math extension such as KaTeX, MathJax, or Pandoc math support.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Alerts
&lt;/h3&gt;

&lt;p&gt;GitHub supports alert-style block quotes such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt; [!WARNING]&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Changing this setting clears the cache.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On GitHub, this can appear as a styled warning. On a plain CommonMark renderer, it usually appears as an ordinary block quote containing &lt;code&gt;[!WARNING]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That fallback is readable, which makes GitHub alerts less dangerous than extensions that disappear completely. They are still not portable presentation elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pandoc Markdown: Markdown as a Document Language
&lt;/h2&gt;

&lt;p&gt;Pandoc Markdown is designed for document conversion rather than one particular website. It uses Markdown as the source syntax for producing HTML, PDF, DOCX, EPUB, LaTeX, presentations, and other formats.&lt;/p&gt;

&lt;p&gt;Its default Markdown reader includes a large extension set. Important capabilities include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;YAML metadata blocks&lt;/li&gt;
&lt;li&gt;Footnotes&lt;/li&gt;
&lt;li&gt;Citations&lt;/li&gt;
&lt;li&gt;Multiple table formats&lt;/li&gt;
&lt;li&gt;Definition lists&lt;/li&gt;
&lt;li&gt;Mathematical notation&lt;/li&gt;
&lt;li&gt;Header identifiers and attributes&lt;/li&gt;
&lt;li&gt;Code block attributes&lt;/li&gt;
&lt;li&gt;Fenced divisions&lt;/li&gt;
&lt;li&gt;Bracketed spans&lt;/li&gt;
&lt;li&gt;Superscript and subscript&lt;/li&gt;
&lt;li&gt;Strikeout&lt;/li&gt;
&lt;li&gt;Line blocks&lt;/li&gt;
&lt;li&gt;Numbered example lists&lt;/li&gt;
&lt;li&gt;Raw LaTeX&lt;/li&gt;
&lt;li&gt;Raw HTML&lt;/li&gt;
&lt;li&gt;Automatic section numbering&lt;/li&gt;
&lt;li&gt;Bibliography processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pandoc Markdown is much more expressive than CommonMark or formal GFM. That expressiveness makes it powerful for publishing, but less safe as an interchange format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pandoc Footnotes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Markdown has several incompatible dialects.[^dialects]

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;^dialects&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="sx"&gt;CommonMark,&lt;/span&gt; GFM, and Pandoc Markdown are three
    important examples.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Footnote syntax is supported by many modern tools, but it is not part of CommonMark or formal GFM.&lt;/p&gt;

&lt;p&gt;GitHub currently renders footnotes in several content contexts, but that is a GitHub platform feature rather than a formal GFM guarantee. A renderer claiming only CommonMark or GFM compatibility may not support them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pandoc Citations
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;PagedAttention improves KV cache memory management
[@kwon2023pagedattention].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a bibliography file and citation style, Pandoc can resolve this into a formatted academic citation and bibliography.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc article.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--citeproc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bibliography&lt;/span&gt; references.bib &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--csl&lt;/span&gt; ieee.csl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; article.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The citation syntax remains readable in an unsupported renderer, but it will not become a formatted reference without Pandoc or another compatible citation processor. Pandoc's reader-side flexibility also underpins conversion workflows in the other direction — see &lt;a href="https://www.glukhov.org/documentation-tools/markdown/converting-word-document-to-markdown/" rel="noopener noreferrer"&gt;converting Word documents to Markdown&lt;/a&gt; for a practical example of using Pandoc's extended dialect as an intermediate format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pandoc Definition Lists
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;CommonMark
: A precise specification for core Markdown.

GFM
: A CommonMark-based dialect with software-oriented extensions.

Pandoc Markdown
: An extended authoring format for document conversion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Definition lists are useful in manuals, glossaries, and technical books. They normally degrade poorly in renderers that do not support them because the colon lines remain visible as plain text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pandoc Header Attributes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Cache Configuration {#cache-config .deployment}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pandoc interprets the braces as an explicit identifier and class list. Many other Markdown renderers show the attribute text directly in the heading.&lt;/p&gt;

&lt;p&gt;This is one of the clearest examples of useful syntax that should not be placed in a document expected to render everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pandoc Fenced Divisions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;::: warning
Changing this option restarts the server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pandoc converts this into a structural division with a class. Templates, CSS, filters, or output writers can decide how that structure should appear.&lt;/p&gt;

&lt;p&gt;Most CommonMark and GFM renderers do not recognize the fence. They display the colons and content as ordinary text.&lt;/p&gt;

&lt;h2&gt;
  
  
  CommonMark vs GFM vs Pandoc Markdown
&lt;/h2&gt;

&lt;p&gt;The following matrix describes the formal dialects, not every feature added by GitHub.com, Hugo, Obsidian, GitLab, or another platform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;CommonMark&lt;/th&gt;
&lt;th&gt;Formal GFM&lt;/th&gt;
&lt;th&gt;Pandoc Markdown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Headings&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emphasis&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links and images&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block quotes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordered and unordered lists&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fenced code blocks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw HTML syntax&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Restricted in some contexts&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipe tables&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task lists&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strikethrough&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extended autolinks&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Configurable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Footnotes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citations&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML metadata&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Definition lists&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mathematical notation&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header attributes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fenced divisions&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw LaTeX&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bibliography processing&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The word "No" does not mean a platform can never support the feature. It means the feature is not guaranteed by that dialect's formal specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Syntax Works on GitHub?
&lt;/h2&gt;

&lt;p&gt;For README files, issues, pull requests, discussions, and wikis, GFM is the natural baseline.&lt;/p&gt;

&lt;p&gt;You can generally use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CommonMark syntax&lt;/li&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Task lists&lt;/li&gt;
&lt;li&gt;Strikethrough&lt;/li&gt;
&lt;li&gt;Extended autolinks&lt;/li&gt;
&lt;li&gt;Syntax-highlighted code fences&lt;/li&gt;
&lt;li&gt;GitHub-specific references&lt;/li&gt;
&lt;li&gt;GitHub-supported math&lt;/li&gt;
&lt;li&gt;GitHub-supported diagrams&lt;/li&gt;
&lt;li&gt;GitHub alerts&lt;/li&gt;
&lt;li&gt;Footnotes where supported by the content surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The portability risk begins when GitHub performs additional rendering beyond formal GFM. Mermaid diagrams, mathematical notation, issue references, and alert presentation may not survive outside GitHub.&lt;/p&gt;

&lt;p&gt;For repository files that are also published elsewhere, test the source in the second renderer rather than treating GitHub preview as authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Syntax Works in Hugo?
&lt;/h2&gt;

&lt;p&gt;Hugo uses Goldmark as its default Markdown renderer. Goldmark conforms to CommonMark and provides extensions compatible with important parts of GFM.&lt;/p&gt;

&lt;p&gt;In a typical Hugo configuration, the following work well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CommonMark structure&lt;/li&gt;
&lt;li&gt;Fenced code blocks&lt;/li&gt;
&lt;li&gt;Pipe tables&lt;/li&gt;
&lt;li&gt;Strikethrough&lt;/li&gt;
&lt;li&gt;Task lists&lt;/li&gt;
&lt;li&gt;Automatic heading IDs&lt;/li&gt;
&lt;li&gt;Syntax highlighting&lt;/li&gt;
&lt;li&gt;Footnotes when the extension is enabled&lt;/li&gt;
&lt;li&gt;Definition lists when enabled&lt;/li&gt;
&lt;li&gt;Typographic substitutions when enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hugo also adds features outside Markdown through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Front matter&lt;/li&gt;
&lt;li&gt;Shortcodes&lt;/li&gt;
&lt;li&gt;Render hooks&lt;/li&gt;
&lt;li&gt;Page resources&lt;/li&gt;
&lt;li&gt;Internal reference functions&lt;/li&gt;
&lt;li&gt;Template processing&lt;/li&gt;
&lt;li&gt;Site configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These Hugo features do not travel with the Markdown file. For a practical example of Hugo deployment, see &lt;a href="https://www.glukhov.org/web-infrastructure/hugo/deploy-hugo-s3/" rel="noopener noreferrer"&gt;Deploy Hugo to AWS S3&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hugo Front Matter Is Not Markdown Content
&lt;/h3&gt;

&lt;p&gt;A Hugo page commonly starts with YAML, TOML, or JSON metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Markdown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Compatibility"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Compare&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Markdown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dialects&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;renderers."&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-07-31&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Markdown&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;documentation&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pandoc can also recognize YAML metadata blocks, but it interprets fields according to its own templates and writers. GitHub normally displays the block as a YAML-like section or treats it as repository metadata only in specific systems.&lt;/p&gt;

&lt;p&gt;The same syntax can therefore be recognized in more than one tool without having the same semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw HTML in Hugo
&lt;/h3&gt;

&lt;p&gt;Goldmark does not render potentially unsafe raw HTML by default in a standard Hugo configuration.&lt;/p&gt;

&lt;p&gt;A block such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"notice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Restart the service after changing this value.
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may be omitted unless raw HTML rendering is enabled or the content is implemented through a shortcode or render hook.&lt;/p&gt;

&lt;p&gt;For a controlled technical blog, enabling raw HTML can be reasonable. It still makes the source less portable and should be a deliberate site-level decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mermaid in Hugo
&lt;/h3&gt;

&lt;p&gt;A fenced &lt;code&gt;mermaid&lt;/code&gt; block is still just a code block unless the Hugo theme, render hook, shortcode, or JavaScript pipeline transforms it into a diagram.&lt;/p&gt;

&lt;p&gt;GitHub and Hugo may therefore accept identical Mermaid source while using completely different rendering mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Syntax Works in Pandoc?
&lt;/h2&gt;

&lt;p&gt;Pandoc can read several Markdown dialects explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;markdown input.md
pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;commonmark input.md
pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gfm input.md
pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;commonmark_x input.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of Pandoc's most useful portability features. The operator can tell Pandoc which dialect the source claims to use instead of relying on a vague &lt;code&gt;.md&lt;/code&gt; file extension.&lt;/p&gt;

&lt;p&gt;Pandoc also lets you enable or disable individual extensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;markdown-footnotes-pipe_tables &lt;span class="se"&gt;\&lt;/span&gt;
  input.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; output.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or start from a narrower format and add one feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;commonmark+footnotes &lt;span class="se"&gt;\&lt;/span&gt;
  input.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; output.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can inspect available extensions with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc &lt;span class="nt"&gt;--list-extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;markdown
pandoc &lt;span class="nt"&gt;--list-extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;commonmark
pandoc &lt;span class="nt"&gt;--list-extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gfm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extension model is powerful, but it means "Pandoc Markdown" is not always one fixed configuration. Build commands and defaults files are part of the document specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Syntax Works in Obsidian?
&lt;/h2&gt;

&lt;p&gt;Obsidian stores notes as Markdown files, but its authoring model includes several application-specific features.&lt;/p&gt;

&lt;p&gt;Common examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wiki links&lt;/li&gt;
&lt;li&gt;Embedded notes&lt;/li&gt;
&lt;li&gt;Embedded files&lt;/li&gt;
&lt;li&gt;Callouts&lt;/li&gt;
&lt;li&gt;Block references&lt;/li&gt;
&lt;li&gt;Tags&lt;/li&gt;
&lt;li&gt;Properties&lt;/li&gt;
&lt;li&gt;Highlighting&lt;/li&gt;
&lt;li&gt;Comments&lt;/li&gt;
&lt;li&gt;Dataview queries from plugins&lt;/li&gt;
&lt;li&gt;Application-specific URI links&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A wiki link such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[[Markdown Compatibility]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is meaningful inside an Obsidian vault. GitHub, CommonMark, and a default Pandoc reader normally display it as literal bracketed text.&lt;/p&gt;

&lt;p&gt;An embed is even more application-specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;![[compatibility-table]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The referenced content is not present in the file itself. Exporting or publishing the note therefore requires an expansion step that resolves the embed.&lt;/p&gt;

&lt;p&gt;Obsidian is a good example of why storage in &lt;code&gt;.md&lt;/code&gt; files does not guarantee Markdown portability. For a practical look at Obsidian as a knowledge management tool, see &lt;a href="https://www.glukhov.org/knowledge-management/tools/obsidian-for-personal-knowledge-management/" rel="noopener noreferrer"&gt;Obsidian for Personal Knowledge Management&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Syntax Works in GitLab?
&lt;/h2&gt;

&lt;p&gt;GitLab Flavored Markdown uses CommonMark as its core and includes GFM features such as tables and task lists. It then adds GitLab-specific behavior including cross-references, mathematical notation, diagrams, and other collaboration features.&lt;/p&gt;

&lt;p&gt;A README written in conservative GFM usually moves between GitHub and GitLab without major damage.&lt;/p&gt;

&lt;p&gt;Platform integrations do not travel as reliably. Issue references, user mentions, diagrams, math handling, and special block syntax can behave differently even when the basic Markdown remains readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform Support Matrix
&lt;/h2&gt;

&lt;p&gt;This matrix describes common default behavior. Themes, plugins, extensions, and configuration can change individual cells.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GitHub&lt;/th&gt;
&lt;th&gt;Hugo Goldmark&lt;/th&gt;
&lt;th&gt;Pandoc&lt;/th&gt;
&lt;th&gt;Obsidian&lt;/th&gt;
&lt;th&gt;GitLab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CommonMark core&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Mostly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipe tables&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task lists&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strikethrough&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Footnotes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Configurable&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML metadata&lt;/td&gt;
&lt;td&gt;Context-dependent&lt;/td&gt;
&lt;td&gt;Front matter&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Properties&lt;/td&gt;
&lt;td&gt;Context-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Requires setup&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Requires setup&lt;/td&gt;
&lt;td&gt;Output-dependent&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citations&lt;/td&gt;
&lt;td&gt;No native bibliography&lt;/td&gt;
&lt;td&gt;Requires tooling&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Plugin-dependent&lt;/td&gt;
&lt;td&gt;No native bibliography&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Definition lists&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Configurable&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header attributes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Renderer-dependent&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wiki links&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No by default&lt;/td&gt;
&lt;td&gt;No by default&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Wiki-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Callouts or alerts&lt;/td&gt;
&lt;td&gt;GitHub syntax&lt;/td&gt;
&lt;td&gt;Theme or shortcode&lt;/td&gt;
&lt;td&gt;Template-dependent&lt;/td&gt;
&lt;td&gt;Obsidian syntax&lt;/td&gt;
&lt;td&gt;GitLab syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw HTML&lt;/td&gt;
&lt;td&gt;Sanitized or restricted&lt;/td&gt;
&lt;td&gt;Disabled by default&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Context-dependent&lt;/td&gt;
&lt;td&gt;Sanitized or restricted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Yes" still does not guarantee identical HTML or visual presentation. It means the environment recognizes the general feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syntax That Is Usually Safe Everywhere
&lt;/h2&gt;

&lt;p&gt;The safest portable subset includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ATX headings using &lt;code&gt;#&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ordinary paragraphs&lt;/li&gt;
&lt;li&gt;Blank lines between blocks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; for unordered lists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1.&lt;/code&gt; for ordered lists&lt;/li&gt;
&lt;li&gt;Fenced code blocks using backticks&lt;/li&gt;
&lt;li&gt;Inline code using backticks&lt;/li&gt;
&lt;li&gt;Emphasis using &lt;code&gt;*text*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Strong emphasis using &lt;code&gt;**text**&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ordinary links&lt;/li&gt;
&lt;li&gt;Ordinary images&lt;/li&gt;
&lt;li&gt;Block quotes&lt;/li&gt;
&lt;li&gt;Thematic breaks&lt;/li&gt;
&lt;li&gt;Explicit angle-bracket autolinks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An intentionally conservative document might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Deployment Guide&lt;/span&gt;

This guide explains how to deploy the service.

&lt;span class="gu"&gt;## Requirements&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Docker
&lt;span class="p"&gt;-&lt;/span&gt; Linux
&lt;span class="p"&gt;-&lt;/span&gt; A supported GPU

&lt;span class="gu"&gt;## Configuration&lt;/span&gt;

Create a file named &lt;span class="sb"&gt;`compose.yaml`&lt;/span&gt;.

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;yaml
&lt;/span&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/application:1.0&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

For more information, see the &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;configuration reference&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;config.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;span class="gt"&gt;
&amp;gt; Back up existing data before upgrading.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This syntax travels well because it does not depend on tables, footnotes, attributes, callouts, or platform processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syntax That Commonly Breaks
&lt;/h2&gt;

&lt;p&gt;Portability problems tend to cluster around a small number of features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pipe Tables
&lt;/h3&gt;

&lt;p&gt;Pipe tables are well supported by GFM-oriented tools, but not by strict CommonMark.&lt;/p&gt;

&lt;p&gt;A table can degrade into unreadable text when passed through a parser that does not recognize it. For highly portable documents, consider short lists or semantic HTML generated during a build step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Footnotes
&lt;/h3&gt;

&lt;p&gt;Footnote syntax has become common, but it remains an extension.&lt;/p&gt;

&lt;p&gt;Different tools may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support only one footnote format&lt;/li&gt;
&lt;li&gt;Place footnotes differently&lt;/li&gt;
&lt;li&gt;Generate different identifiers&lt;/li&gt;
&lt;li&gt;Reject multi-paragraph footnotes&lt;/li&gt;
&lt;li&gt;Render the source literally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use footnotes when the publishing pipeline is known. Avoid depending on them in README files that must render across arbitrary systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Heading IDs and Attributes
&lt;/h3&gt;

&lt;p&gt;This Pandoc syntax is not portable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Installation {#installation .procedure}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an ordinary heading and let the renderer generate its own anchor when portability matters.&lt;/p&gt;

&lt;p&gt;Also avoid hard-coding links to auto-generated heading IDs unless every target uses the same slugification rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Callouts and Alerts
&lt;/h3&gt;

&lt;p&gt;GitHub, Obsidian, GitLab, MkDocs, Docusaurus, and Hugo themes can all support callout-like blocks, but they often use different syntax.&lt;/p&gt;

&lt;p&gt;A portable fallback is an ordinary block quote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt; Warning: Back up the database before upgrading.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is less visually impressive, but it preserves meaning everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiki Links
&lt;/h3&gt;

&lt;p&gt;Wiki links are concise inside knowledge-management tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[[KV Cache]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are poor interchange syntax because the target path, file name, heading rules, and resolution behavior belong to the application.&lt;/p&gt;

&lt;p&gt;Use standard Markdown links in content intended for publication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;KV cache&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;kv-cache.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Raw HTML
&lt;/h3&gt;

&lt;p&gt;Raw HTML is the usual escape hatch when Markdown cannot express a layout. It is also a common portability and security failure.&lt;/p&gt;

&lt;p&gt;A renderer may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove the HTML&lt;/li&gt;
&lt;li&gt;Escape it&lt;/li&gt;
&lt;li&gt;Sanitize selected elements&lt;/li&gt;
&lt;li&gt;Allow blocks but not inline elements&lt;/li&gt;
&lt;li&gt;Refuse Markdown parsing inside HTML&lt;/li&gt;
&lt;li&gt;Pass it unchanged only in trusted mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use raw HTML only when the publishing target is controlled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mathematical Notation
&lt;/h3&gt;

&lt;p&gt;Dollar-delimited math is popular but not universally interpreted.&lt;/p&gt;

&lt;p&gt;The source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;The complexity is $O(n^2)$.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rendered mathematics&lt;/li&gt;
&lt;li&gt;Ordinary text with dollar signs&lt;/li&gt;
&lt;li&gt;Incorrect emphasis&lt;/li&gt;
&lt;li&gt;Input to a different math parser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose one math pipeline and test it in every target environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mermaid and Other Diagram Blocks
&lt;/h3&gt;

&lt;p&gt;A Mermaid code fence is syntactically safe because unsupported renderers normally display it as code.&lt;/p&gt;

&lt;p&gt;The semantic result is still different. Readers may see a rendered architecture diagram on GitHub and raw Mermaid source in another environment.&lt;/p&gt;

&lt;p&gt;This is graceful degradation, not true compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Layers of Markdown Compatibility
&lt;/h2&gt;

&lt;p&gt;It helps to separate compatibility into three layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Parsing Compatibility
&lt;/h3&gt;

&lt;p&gt;Does the parser recognize the structure?&lt;/p&gt;

&lt;p&gt;Examples include headings, tables, footnotes, and fenced divisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Transformation Compatibility
&lt;/h3&gt;

&lt;p&gt;Does the platform apply additional processing?&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rendering Mermaid&lt;/li&gt;
&lt;li&gt;Resolving citations&lt;/li&gt;
&lt;li&gt;Expanding wiki links&lt;/li&gt;
&lt;li&gt;Linking issue numbers&lt;/li&gt;
&lt;li&gt;Processing shortcodes&lt;/li&gt;
&lt;li&gt;Generating a table of contents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 3: Presentation Compatibility
&lt;/h3&gt;

&lt;p&gt;Does the result look and behave appropriately?&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table styling&lt;/li&gt;
&lt;li&gt;Syntax highlighting&lt;/li&gt;
&lt;li&gt;Alert colors&lt;/li&gt;
&lt;li&gt;Heading anchors&lt;/li&gt;
&lt;li&gt;Responsive images&lt;/li&gt;
&lt;li&gt;Footnote placement&lt;/li&gt;
&lt;li&gt;Math fonts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two platforms can parse identical syntax while producing substantially different presentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Portability Model
&lt;/h2&gt;

&lt;p&gt;Instead of asking whether a file is "valid Markdown," ask four narrower questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which dialect is the source written in?&lt;/li&gt;
&lt;li&gt;Which parser reads it?&lt;/li&gt;
&lt;li&gt;Which extensions are enabled?&lt;/li&gt;
&lt;li&gt;Which platform transformations run afterward?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dialect: CommonMark plus GFM tables
Parser: Goldmark
Extensions: tables, strikethrough, task lists, footnotes
Platform: Hugo
Additional processing: render hooks and Mermaid JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That description is much more useful than saying "the site uses Markdown."&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a Dialect by Use Case
&lt;/h2&gt;

&lt;h3&gt;
  
  
  README Files
&lt;/h3&gt;

&lt;p&gt;Use GFM.&lt;/p&gt;

&lt;p&gt;README files benefit from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Task lists&lt;/li&gt;
&lt;li&gt;Fenced code&lt;/li&gt;
&lt;li&gt;Autolinks&lt;/li&gt;
&lt;li&gt;Strikethrough&lt;/li&gt;
&lt;li&gt;GitHub references&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid excessive dependence on GitHub-only features when the repository is mirrored to GitLab, rendered on a package registry, or included in generated documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hugo Technical Articles
&lt;/h3&gt;

&lt;p&gt;Use CommonMark-compatible Markdown with a documented Goldmark extension set.&lt;/p&gt;

&lt;p&gt;Tables, code fences, footnotes, and Mermaid can be reasonable because you control the build pipeline. Prefer Hugo shortcodes or render hooks over embedding large amounts of raw HTML.&lt;/p&gt;

&lt;p&gt;Keep Hugo-specific syntax isolated and easy to find.&lt;/p&gt;

&lt;h3&gt;
  
  
  Academic Documents
&lt;/h3&gt;

&lt;p&gt;Use Pandoc Markdown.&lt;/p&gt;

&lt;p&gt;Citations, bibliography processing, footnotes, metadata, mathematical notation, cross-references, and conversion to PDF or DOCX justify the reduced portability.&lt;/p&gt;

&lt;p&gt;Store the Pandoc command, defaults file, filters, bibliography, and templates beside the source. The source file alone does not fully describe the build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Books and Long-Form Documentation
&lt;/h3&gt;

&lt;p&gt;Pandoc Markdown is usually the strongest of the three options when multiple output formats matter.&lt;/p&gt;

&lt;p&gt;Definition lists, citations, attributes, metadata, and structured transformations become more important as document complexity grows.&lt;/p&gt;

&lt;p&gt;For web-only documentation hosted in a Git repository, GFM or a CommonMark-based documentation generator may remain simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notes and Personal Knowledge Bases
&lt;/h3&gt;

&lt;p&gt;Use the native syntax of the selected notes application when application features provide real value.&lt;/p&gt;

&lt;p&gt;Obsidian wiki links, embeds, and callouts are useful inside a vault. Treat export as a compilation process rather than assuming the raw files are already portable publications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Documentation Across Unknown Systems
&lt;/h3&gt;

&lt;p&gt;Use a conservative CommonMark subset.&lt;/p&gt;

&lt;p&gt;Avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wiki links&lt;/li&gt;
&lt;li&gt;Platform alerts&lt;/li&gt;
&lt;li&gt;Header attributes&lt;/li&gt;
&lt;li&gt;Citations&lt;/li&gt;
&lt;li&gt;Raw HTML&lt;/li&gt;
&lt;li&gt;Custom containers&lt;/li&gt;
&lt;li&gt;Application embeds&lt;/li&gt;
&lt;li&gt;Shortcodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Portability usually requires giving up convenience features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Rules for Portable Markdown
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with CommonMark Structure
&lt;/h3&gt;

&lt;p&gt;Use CommonMark for the document skeleton:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Headings&lt;/li&gt;
&lt;li&gt;Paragraphs&lt;/li&gt;
&lt;li&gt;Lists&lt;/li&gt;
&lt;li&gt;Links&lt;/li&gt;
&lt;li&gt;Images&lt;/li&gt;
&lt;li&gt;Block quotes&lt;/li&gt;
&lt;li&gt;Code blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures that the main meaning survives even when optional extensions fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add GFM Features Deliberately
&lt;/h3&gt;

&lt;p&gt;Tables and task lists are reasonable when all important targets support them.&lt;/p&gt;

&lt;p&gt;Do not assume "most tools support GFM" without testing the exact target. Some claim GFM compatibility while enabling only selected extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolate Platform Extensions
&lt;/h3&gt;

&lt;p&gt;Keep platform-specific syntax in clearly identifiable blocks.&lt;/p&gt;

&lt;p&gt;For example, centralize Hugo shortcodes, Pandoc citations, or Obsidian embeds rather than scattering them through every paragraph.&lt;/p&gt;

&lt;p&gt;Isolation makes later conversion easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefer Graceful Degradation
&lt;/h3&gt;

&lt;p&gt;A Mermaid block degrades into readable source code. A GitHub alert degrades into a block quote.&lt;/p&gt;

&lt;p&gt;A wiki embed may degrade into an unexplained file name, while a Pandoc fenced division may expose punctuation around the content.&lt;/p&gt;

&lt;p&gt;Choose extensions whose fallback remains understandable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Depend on Auto-Generated Heading IDs
&lt;/h3&gt;

&lt;p&gt;Heading anchor algorithms differ between GitHub, Hugo, Pandoc, and documentation generators.&lt;/p&gt;

&lt;p&gt;For cross-document links, use renderer-supported explicit IDs only when the target pipeline is controlled. Otherwise, link to the document rather than a generated fragment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Build Configuration with the Content
&lt;/h3&gt;

&lt;p&gt;Pandoc extensions, Hugo settings, plugins, filters, and JavaScript integrations determine how Markdown behaves.&lt;/p&gt;

&lt;p&gt;Commit relevant configuration files with the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content/
  article.md
pandoc.yaml
references.bib
config/
  _default/
    markup.yaml
layouts/
  _default/
    _markup/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;.md&lt;/code&gt; extension alone does not capture the publishing environment. For a structured approach to documenting these decisions, see &lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;Decision Records for AI-Driven Development&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Markdown Against Every Important Target
&lt;/h2&gt;

&lt;p&gt;Visual preview in one editor is not enough. The editor may support a richer dialect than the production renderer.&lt;/p&gt;

&lt;p&gt;For Pandoc, test explicit input formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;commonmark article.md &lt;span class="nt"&gt;-o&lt;/span&gt; commonmark.html
pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gfm article.md &lt;span class="nt"&gt;-o&lt;/span&gt; gfm.html
pandoc &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;markdown article.md &lt;span class="nt"&gt;-o&lt;/span&gt; pandoc.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Warnings and visible source punctuation reveal which features are dialect-specific.&lt;/p&gt;

&lt;p&gt;For Hugo, build the production site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hugo &lt;span class="nt"&gt;--gc&lt;/span&gt; &lt;span class="nt"&gt;--minify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the generated HTML rather than relying only on an editor preview.&lt;/p&gt;

&lt;p&gt;For repositories, view the committed file on the actual hosting platform. Local Markdown extensions in VS Code may not match GitHub or GitLab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Rendering Mismatches
&lt;/h2&gt;

&lt;p&gt;When a file that worked on one platform breaks on another, the failure usually falls into one of a handful of repeatable patterns. The table below lists the symptom as you would actually see it, the most likely cause, and a concrete command or check to confirm and fix it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Confirm and fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A pipe table renders as one long paragraph with visible `\&lt;/td&gt;
&lt;td&gt;` characters&lt;/td&gt;
&lt;td&gt;Renderer is strict CommonMark without a tables extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[^note]&lt;/code&gt; stays inline as literal text instead of becoming a superscript footnote marker&lt;/td&gt;
&lt;td&gt;The footnote Goldmark extension is not enabled&lt;/td&gt;
&lt;td&gt;In Hugo, check for &lt;code&gt;footnote&lt;/code&gt; under &lt;code&gt;markup.goldmark.extensions&lt;/code&gt; in &lt;code&gt;hugo.yaml&lt;/code&gt;, rebuild with &lt;code&gt;hugo --gc --minify&lt;/code&gt;, and look for &lt;code&gt;&amp;lt;sup&amp;gt;&lt;/code&gt; in the generated HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;&lt;/code&gt;`&lt;code&gt;mermaid&lt;/code&gt; fence shows as plain grey source code instead of a diagram&lt;/td&gt;
&lt;td&gt;The platform performs no post-processing on the fenced block&lt;/td&gt;
&lt;td&gt;GitHub renders it natively; Hugo needs a render hook, shortcode, or JS pipeline — check the built HTML for &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code class="language-mermaid"&amp;gt;&lt;/code&gt; versus an &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;## Heading {#id}&lt;/code&gt; shows the literal curly braces in the rendered heading text&lt;/td&gt;
&lt;td&gt;Header attribute syntax is Pandoc-specific, not CommonMark or GFM&lt;/td&gt;
&lt;td&gt;Remove the attribute syntax for portable output, or pre-convert with &lt;code&gt;pandoc --from=markdown --to=gfm file.md -o out.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[[Note Name]]&lt;/code&gt; displays as literal double square brackets&lt;/td&gt;
&lt;td&gt;Wiki link syntax is application-specific to tools like Obsidian&lt;/td&gt;
&lt;td&gt;Replace with a standard Markdown link, &lt;code&gt;[Note Name](note-name.md)&lt;/code&gt;, before exporting outside the vault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[@kwon2023pagedattention]&lt;/code&gt; stays as plain bracketed text instead of a formatted citation&lt;/td&gt;
&lt;td&gt;No bibliography or citeproc pass was applied&lt;/td&gt;
&lt;td&gt;Re-run with &lt;code&gt;pandoc --citeproc --bibliography=refs.bib input.md -o output.pdf&lt;/code&gt; and confirm the CSL style is specified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;gt; [!WARNING]&lt;/code&gt; renders as an ordinary quoted paragraph instead of a styled alert&lt;/td&gt;
&lt;td&gt;Alert styling is a GitHub.com platform feature, not part of formal GFM&lt;/td&gt;
&lt;td&gt;Expected outside GitHub; keep the wording readable as a plain block quote rather than depending on the color styling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the fastest first pass before assuming a Markdown "bug" — most of these mismatches are a missing extension or a platform-only feature, not broken syntax. For code-fence-specific issues such as missing syntax highlighting or unsupported language identifiers, see the dedicated guide on &lt;a href="https://www.glukhov.org/documentation-tools/markdown/markdown-codeblocks/" rel="noopener noreferrer"&gt;Markdown code blocks&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lint the Portable Subset
&lt;/h2&gt;

&lt;p&gt;A Markdown linter cannot guarantee renderer compatibility, but it can remove avoidable ambiguity.&lt;/p&gt;

&lt;p&gt;Useful rules include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use consistent heading styles&lt;/li&gt;
&lt;li&gt;Add blank lines around lists and code blocks&lt;/li&gt;
&lt;li&gt;Use fenced rather than indented code&lt;/li&gt;
&lt;li&gt;Specify code fence languages&lt;/li&gt;
&lt;li&gt;Avoid skipped heading levels&lt;/li&gt;
&lt;li&gt;Use consistent list markers&lt;/li&gt;
&lt;li&gt;Avoid ambiguous emphasis around punctuation&lt;/li&gt;
&lt;li&gt;Keep line endings consistent&lt;/li&gt;
&lt;li&gt;Validate links and images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For multi-target publishing, add a build test for each important renderer rather than relying only on syntax linting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting Between Dialects with Pandoc
&lt;/h2&gt;

&lt;p&gt;Pandoc can normalize documents from one dialect to another:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
pandoc \&lt;br&gt;
  --from=markdown \&lt;br&gt;
  --to=gfm \&lt;br&gt;
  article.md \&lt;br&gt;
  -o article-gfm.md&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or convert GFM into Pandoc Markdown:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
pandoc \&lt;br&gt;
  --from=gfm \&lt;br&gt;
  --to=markdown \&lt;br&gt;
  README.md \&lt;br&gt;
  -o document.md&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is useful, but conversion is not guaranteed to preserve every feature.&lt;/p&gt;

&lt;p&gt;Potential losses include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform-specific references&lt;/li&gt;
&lt;li&gt;Callout styling&lt;/li&gt;
&lt;li&gt;Complex tables&lt;/li&gt;
&lt;li&gt;Embedded application objects&lt;/li&gt;
&lt;li&gt;Custom attributes&lt;/li&gt;
&lt;li&gt;Raw HTML behavior&lt;/li&gt;
&lt;li&gt;Plugin syntax&lt;/li&gt;
&lt;li&gt;Diagram rendering&lt;/li&gt;
&lt;li&gt;Exact whitespace and formatting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pandoc preserves document structure better than original source formatting. Treat conversion as a build step, not a reversible text formatter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Strategy for Hugo Sites
&lt;/h2&gt;

&lt;p&gt;For a Hugo technical blog, the most practical policy is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use CommonMark for core prose and structure.&lt;/li&gt;
&lt;li&gt;Enable a small documented set of Goldmark extensions.&lt;/li&gt;
&lt;li&gt;Use GFM-style tables and task lists where they improve readability.&lt;/li&gt;
&lt;li&gt;Implement Mermaid through one consistent render hook or shortcode.&lt;/li&gt;
&lt;li&gt;Handle math through one documented KaTeX or MathJax pipeline.&lt;/li&gt;
&lt;li&gt;Use Hugo front matter only at the start of content files.&lt;/li&gt;
&lt;li&gt;Prefer render hooks and shortcodes over raw HTML.&lt;/li&gt;
&lt;li&gt;Keep source links as standard Markdown links where possible.&lt;/li&gt;
&lt;li&gt;Test migrated or externally sourced documents through Hugo.&lt;/li&gt;
&lt;li&gt;Document any syntax that will not render correctly on GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach accepts that Hugo content is not universally portable while keeping the portability boundary visible.&lt;/p&gt;

&lt;p&gt;The worst approach is accidental dialect mixing: GitHub alerts, Obsidian embeds, Pandoc attributes, and Hugo shortcodes placed in the same document without a defined build pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Recommended syntax&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Portable plain-text document&lt;/td&gt;
&lt;td&gt;CommonMark&lt;/td&gt;
&lt;td&gt;Smallest reliable baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub README&lt;/td&gt;
&lt;td&gt;GFM&lt;/td&gt;
&lt;td&gt;Tables, tasks, and repository workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub issue template&lt;/td&gt;
&lt;td&gt;GFM plus GitHub features&lt;/td&gt;
&lt;td&gt;Platform is the intended target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugo blog post&lt;/td&gt;
&lt;td&gt;CommonMark plus configured Goldmark extensions&lt;/td&gt;
&lt;td&gt;Controlled publishing pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Academic paper&lt;/td&gt;
&lt;td&gt;Pandoc Markdown&lt;/td&gt;
&lt;td&gt;Citations, math, metadata, PDF output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-format book&lt;/td&gt;
&lt;td&gt;Pandoc Markdown&lt;/td&gt;
&lt;td&gt;Structured conversion to many outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Obsidian vault&lt;/td&gt;
&lt;td&gt;Obsidian Markdown&lt;/td&gt;
&lt;td&gt;Backlinks, embeds, and knowledge workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub and GitLab mirror&lt;/td&gt;
&lt;td&gt;Conservative GFM&lt;/td&gt;
&lt;td&gt;Strong shared feature set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown renderer&lt;/td&gt;
&lt;td&gt;CommonMark subset&lt;/td&gt;
&lt;td&gt;Lowest compatibility risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CommonMark, GitHub Flavored Markdown, and Pandoc Markdown are not competing versions of the same product. They solve different problems.&lt;/p&gt;

&lt;p&gt;CommonMark provides a dependable parsing foundation. GFM adds practical features for software collaboration, while Pandoc Markdown turns Markdown into a rich source language for publishing and conversion.&lt;/p&gt;

&lt;p&gt;The safest rule is simple: write the smallest dialect that satisfies the real destination. Use CommonMark when content must travel, GFM when GitHub-style collaboration is the target, and Pandoc Markdown when document structure and output formats matter more than universal rendering.&lt;/p&gt;

&lt;p&gt;Markdown portability is not achieved by avoiding every extension. It is achieved by knowing which extensions are part of the source contract and testing them in every renderer that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;CommonMark Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.github.com/gfm/" rel="noopener noreferrer"&gt;GitHub Flavored Markdown Spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pandoc.org/MANUAL.html#pandocs-markdown" rel="noopener noreferrer"&gt;Pandoc Markdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/yuin/goldmark" rel="noopener noreferrer"&gt;Goldmark Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/getting-started/configuration-markup/" rel="noopener noreferrer"&gt;Hugo Goldmark Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions" rel="noopener noreferrer"&gt;Writing Mathematical Expressions on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>markdown</category>
      <category>hugo</category>
      <category>documentation</category>
      <category>wiki</category>
    </item>
    <item>
      <title>Data Gravity: The Real Cost of API-First AI</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:54:30 +0000</pubDate>
      <link>https://dev.to/rosgluk/data-gravity-the-real-cost-of-api-first-ai-1p88</link>
      <guid>https://dev.to/rosgluk/data-gravity-the-real-cost-of-api-first-ai-1p88</guid>
      <description>&lt;p&gt;Every API call feels like a simple transaction - until enough of them accumulate that your fine-tuning data, evaluation harnesses, and tool schemas are all shaped around one vendor, and switching stops being a routing change.&lt;/p&gt;

&lt;p&gt;That's data gravity: the same force that made pulling data out of AWS S3 expensive long before AI existed, now operating one layer up the &lt;a href="https://www.glukhov.org/llm-hosting/" rel="noopener noreferrer"&gt;LLM hosting&lt;/a&gt; stack. It doesn't require a bad contract or a malicious vendor. It's compounding integration debt - every fine-tuned checkpoint, cached embedding, and evaluation harness tuned to one provider's output format makes the next one cheaper to add and the whole pile more expensive to move.&lt;/p&gt;

&lt;p&gt;The mechanism has four stages, and none of them announce themselves. Most teams don't decide to become dependent - they drift from Exploration into Integration, then Optimization, until Dependency feels less like a choice and more like the ground truth of their architecture. Recognizing which stage you're in, and what it costs to reverse, is the point of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mechanism: Four Stages of Lock-In
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    A[Exploration&amp;lt;br/&amp;gt;swap a base URL] --&amp;gt; B[Integration&amp;lt;br/&amp;gt;workflows assume&amp;lt;br/&amp;gt;the API's shape]
    B --&amp;gt; C[Optimization&amp;lt;br/&amp;gt;fine-tunes, caches,&amp;lt;br/&amp;gt;vector stores]
    C --&amp;gt; D[Dependency&amp;lt;br/&amp;gt;product quality =&amp;lt;br/&amp;gt;vendor's model]
    style A fill:#e8f4fd
    style B fill:#cfe8fb
    style C fill:#a8d4f5
    style D fill:#6fb3ea
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Exploration.&lt;/strong&gt; You call an API, prototype, iterate. Switching cost is low - changing a base URL and a key covers most of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration.&lt;/strong&gt; You build workflows around the API's shape. Error handling assumes its rate-limit headers. Retry logic matches its backoff curves. Your evaluation harness is tuned to its output format. Switching now means refactoring, not routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimization.&lt;/strong&gt; You fine-tune. You cache. You build vector stores and custom pipelines that depend on that provider's embedding space, tokenization, or tool-calling schema. Your data is embedded in their ecosystem. Switching means rebuilding, not refactoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency.&lt;/strong&gt; Your product's performance depends on that provider's model quality. Downgrading to a self-hosted alternative means accepting lower capability. The trade-off stops being architectural and becomes product-level.&lt;/p&gt;

&lt;p&gt;Each stage compounds the previous one. The transition from Exploration to Dependency rarely feels like a decision - it feels like progress, right up until the moment a vendor changes the terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters Right Now
&lt;/h2&gt;

&lt;p&gt;Three forces are making data gravity urgent instead of theoretical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-weight models are closing the capability gap.&lt;/strong&gt; Moonshot AI's Kimi K3, a 2.8-trillion-parameter sparse mixture-of-experts model released in July 2026, scored 57 on the &lt;a href="https://artificialanalysis.ai/articles/kimi-k3-achieves-3-in-the-artificial-analysis-intelligence-index-comparable-to-opus-4-8-and-gpt-5-5" rel="noopener noreferrer"&gt;Artificial Analysis Intelligence Index&lt;/a&gt; - third overall, comparable to Claude Opus 4.8 and GPT-5.5, and still behind Claude Fable 5 and GPT-5.6 Sol, but close enough that the gap is now a deliberate trade-off rather than a forced compromise. Qwen and DeepSeek ship under permissive licenses with native support across &lt;a href="https://www.glukhov.org/llm-hosting/vllm/vllm-quickstart/" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt; and &lt;a href="https://www.glukhov.org/llm-hosting/sglang/" rel="noopener noreferrer"&gt;SGLang&lt;/a&gt;. For coding and infrastructure tasks specifically, open-weight models routinely land within 5-15% of frontier API quality - close enough that the lock-in cost, not the capability gap, becomes the deciding factor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geopolitics is fragmenting data flows.&lt;/strong&gt; In July 2026, security researchers found that Claude Code had shipped hidden detection code since version 2.1.91 (April 2, 2026) that checked a user's system timezone against &lt;code&gt;Asia/Shanghai&lt;/code&gt; and &lt;code&gt;Asia/Urumqi&lt;/code&gt; and scanned proxy hostnames against a list of Chinese corporate and AI-lab domains - including Alibaba, Baidu, ByteDance, and Moonshot AI - encoding the match invisibly into the tool's own system prompt. Anthropic called it an anti-distillation experiment; Alibaba responded by banning Claude Code for its employees effective July 10, 2026, and ordering Claude models deleted from company infrastructure. Whatever the intent, the episode is a preview of a world where cross-border AI data flows carry protocol-level risk, not just contractual risk. If your data and your tool's behavior live in someone else's runtime, you're subject to decisions you can't audit - which is the same conclusion &lt;a href="https://www.glukhov.org/llm-hosting/self-hosting/llm-selfhosting-and-ai-sovereignty/" rel="noopener noreferrer"&gt;LLM Self-Hosting and AI Sovereignty&lt;/a&gt; reaches from the policy and jurisdiction side rather than the switching-cost side covered here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory economics are tightening.&lt;/strong&gt; SK Hynix CEO Kwak Noh-jung told Reuters in July 2026 that 2027 will be the memory industry's worst-ever supply shortage, with customer demand expected to outstrip production capacity "even beyond 2030." SambaNova closed the first tranche of a $1B Series F at an $11B valuation the same month, explicitly to scale inference hardware manufacturing. The narrative that API costs fall indefinitely was already shaky; a multi-year hardware shortage makes owning your inference stack a strategic hedge rather than a hobbyist preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cost Isn't Tokens
&lt;/h2&gt;

&lt;p&gt;The pricing comparison is the wrong frame. It's not "$0.01 per 1K input tokens vs. $0.002 self-hosted" - it's architectural dependency, and the clearest recent proof is &lt;a href="https://www.glukhov.org/ai-systems/openclaw/openclaw-rise-and-fall-timeline/" rel="noopener noreferrer"&gt;OpenClaw's collapse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;OpenClaw grew to roughly 247,000 GitHub stars on the strength of running Claude through flat-rate Pro and Max subscriptions rather than metered API billing. On April 4, 2026, Anthropic revoked the ability to use those subscription OAuth tokens in third-party tools. Users who wanted to keep running OpenClaw with Claude had to switch to pay-as-you-go billing at 10 to 50 times the effective cost of their old plan. That is Dependency, stage four, made visible almost overnight: a huge community had optimized its entire workflow around one provider's specific pricing mechanism, and when that mechanism disappeared, the workflow's economics didn't degrade gracefully - they broke. The &lt;a href="https://www.glukhov.org/ai-systems/comparisons/openclaw-hermes-alternatives-popularity/" rel="noopener noreferrer"&gt;OpenClaw vs. Hermes usage data&lt;/a&gt; shows a meaningful share of that traffic migrating toward self-hosted and open-weight alternatives in the months after.&lt;/p&gt;

&lt;p&gt;The same pattern shows up quietly inside individual companies. A team that builds a code-review agent against one provider's API accumulates fine-tuning data in that provider's format, an evaluation harness tuned to that provider's output shape, and tool-calling integrations built around that provider's schema. None of that is measured in tokens. It's measured in engineering weeks the day you try to leave - the same architectural-dependency problem that the &lt;a href="https://www.glukhov.org/llm-architecture/" rel="noopener noreferrer"&gt;LLM Architecture&lt;/a&gt; cluster covers at the routing, cost, and guardrail layer above hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Score Your Lock-In
&lt;/h2&gt;

&lt;p&gt;Count how many of these your team has accumulated for a given provider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dependency&lt;/th&gt;
&lt;th&gt;Do you have this?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fine-tuning datasets stored in a provider-specific format&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached embeddings tied to a provider's embedding space&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation harnesses tuned to a provider's output shape&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom tool schemas built around a provider's tool-calling API&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team knowledge specific to a provider's failure modes and workarounds&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product features that assume a specific model's capability ceiling&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing or usage patterns tied to a provider-specific plan (subscription vs. metered)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;0-2 checked: Exploration - switching cost is still close to zero. 3-5: Integration - expect a real refactor. 6+: Optimization or Dependency - you are not choosing your AI provider anymore; you are renting your architecture from them. The count itself is the warning signal, and it costs nothing to calculate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Self-Hosting Actually Costs - and Doesn't
&lt;/h2&gt;

&lt;p&gt;The economics are real but secondary to the lock-in question. &lt;a href="https://www.glukhov.org/llm-architecture/cost-optimization/cost-optimization-for-llm-systems/" rel="noopener noreferrer"&gt;Cost Optimization for LLM Systems&lt;/a&gt; works through the hardware break-even math in detail - at roughly an hour or more of daily local usage, a consumer GPU like an RTX 4090 typically pays for itself against equivalent API spend within 4-8 months. That analysis is the right place for the $/token comparison; the point worth repeating here is that the break-even calculation only matters once you've decided portability is worth optimizing for. Teams deep in the Dependency stage often find the migration cost dwarfs any hardware savings, which is exactly the trap this article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Antidote: Portability as a Strategy
&lt;/h2&gt;

&lt;p&gt;The goal isn't to avoid APIs. It's to keep your data layer portable long enough to make deliberate choices instead of drifting into a stage you didn't choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start local, go remote deliberately.&lt;/strong&gt; Prototype with self-hosted models - &lt;a href="https://www.glukhov.org/llm-hosting/llama-cpp/" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;, GGUF quantization, or a full comparison of &lt;a href="https://www.glukhov.org/llm-hosting/comparisons/hosting-llms-ollama-localai-jan-lmstudio-vllm-comparison/" rel="noopener noreferrer"&gt;local hosting tools&lt;/a&gt; to pick a stack. When a task genuinely needs frontier capability, use the API for that task specifically - but keep the data layer decoupled from which model answered it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer open-weight to closed-API when the quality gap is close.&lt;/strong&gt; When a model ships as open weights - Kimi K3, Qwen, Gemma, DeepSeek - you can run it, fine-tune it, quantize it, and own the relationship end to end. The capability gap is a known, shrinking, task-dependent trade-off. The lock-in gap is a slow-moving trap that doesn't announce its size until you try to leave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build abstraction where it actually matters.&lt;/strong&gt; Not "wrap everything behind an interface" - that's a rule that delays the problem without solving it. Build the abstraction around data formats, evaluation logic, and tool schemas specifically: fine-tuning datasets in framework-agnostic formats (JSONL, parquet), evaluation harnesses that score model output rather than a specific API's response shape, and tool-calling logic that translates to and from provider-specific schemas rather than being written against one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route deliberately instead of committing to one provider.&lt;/strong&gt; &lt;a href="https://www.glukhov.org/llm-architecture/model-routing/model-routing-strategies/" rel="noopener noreferrer"&gt;Model routing strategies&lt;/a&gt; - capability-based, cost-aware, latency-aware - let you send routine traffic to a local model and edge cases to a frontier API, which keeps you inside the Integration stage indefinitely instead of drifting into Optimization around a single vendor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quantify your lock-in on a schedule.&lt;/strong&gt; Re-run the scoring table above quarterly per provider. When the count climbs, that's data gravity doing its work whether or not anyone made an explicit decision to let it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;A practical stack that resists data gravity by design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inference:&lt;/strong&gt; llama.cpp for local, single-machine serving; vLLM or SGLang for production-grade self-hosted throughput. All three expose OpenAI-compatible APIs, so application code doesn't need to know which one is behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning:&lt;/strong&gt; datasets stored in standard formats - JSONL, parquet - never a provider's proprietary fine-tuning-job format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation:&lt;/strong&gt; framework-agnostic harnesses that score outputs, not API response envelopes, so the same eval suite runs whether the model is local or remote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calling:&lt;/strong&gt; a provider-agnostic JSON schema translated to and from each vendor's tool-calling format, rather than application logic written directly against one vendor's shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector stores:&lt;/strong&gt; local-first options such as Qdrant, Milvus, or Chroma, with embeddings computed through a portable library rather than tied to one provider's embedding endpoint - see &lt;a href="https://www.glukhov.org/rag/retrieval/chunking-strategies-in-rag/" rel="noopener noreferrer"&gt;chunking strategies in RAG&lt;/a&gt; for how this fits the retrieval layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a self-hosting manifesto. Plenty of workloads belong on a frontier API, permanently. It's a recognition that the engineers who can measure and manage data gravity - rather than discover it the day a vendor changes its pricing - end up with more options, not fewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Data gravity is why open-weight capability matters more than a single benchmark score. A model that runs locally at 85-95% of a frontier model's quality is often the better architectural choice, because you keep the data relationship. The frontier race between GPT-5.6 Sol, Fable 5, Kimi K3, and Qwen is genuinely interesting, but the infrastructure layer underneath it - who holds the fine-tuning data, whose schema the tools speak, whose pricing model the workflow assumes - is what actually determines which teams have options in three years and which ones are renting their architecture from someone else.&lt;/p&gt;

&lt;p&gt;Score your lock-in before a vendor's pricing page forces the question for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://artificialanalysis.ai/articles/kimi-k3-achieves-3-in-the-artificial-analysis-intelligence-index-comparable-to-opus-4-8-and-gpt-5-5" rel="noopener noreferrer"&gt;Kimi K3 achieves #3 in the Artificial Analysis Intelligence Index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thenextweb.com/news/alibaba-bans-claude-code-anthropic-tracking-chinese-users" rel="noopener noreferrer"&gt;Alibaba bans Claude Code after Anthropic is caught tracking Chinese users with hidden code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tomshardware.com/pc-components/dram/sk-hynix-says-2027-will-be-the-worst-year-for-memory-shortage-forecasts-crunch-to-last-until-2030-ceo-shares-grim-outlook-on-the-day-sk-hynix-gets-listed-on-nasdaq" rel="noopener noreferrer"&gt;SK Hynix says 2027 will be the 'worst year' for memory shortage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sambanova.ai/press/sambanova-completes-first-close-of-1b-financing-at-11b-valuation" rel="noopener noreferrer"&gt;SambaNova Completes First Close of $1 Billion Financing at $11 Billion Valuation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Claude Code Subagents: Setup, Config, and When to Use Them</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Wed, 05 Aug 2026 09:07:21 +0000</pubDate>
      <link>https://dev.to/rosgluk/claude-code-subagents-setup-config-and-when-to-use-them-44m7</link>
      <guid>https://dev.to/rosgluk/claude-code-subagents-setup-config-and-when-to-use-them-44m7</guid>
      <description>&lt;p&gt;Most Claude Code sessions get slow and cluttered for the same reason: every exploratory grep, every log dump, and every "let me check one more file" stays in the main conversation forever.&lt;/p&gt;

&lt;p&gt;Subagents exist to fix exactly that problem. They are one of the agent primitives built into &lt;a href="https://www.glukhov.org/ai-devtools/claude-code/" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; for handling noisy, parallelizable work — a way to push the mess into an isolated window and bring back only the summary that matters.&lt;/p&gt;

&lt;p&gt;A subagent is not a smarter Claude, and it is not the same thing as a Skill. It is a separate reasoning agent with its own context window, its own tool allowlist, and no memory of your current conversation unless you explicitly fork it. Understanding that distinction is the difference between a subagent setup that quietly saves you context budget and one that just adds latency for no benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subagents vs Skills vs MCP
&lt;/h2&gt;

&lt;p&gt;Claude Code gives you three extension points that solve different problems, and they get conflated constantly because all three can technically "help with a task."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;When to reach for it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;Instructions loaded into the &lt;em&gt;main&lt;/em&gt; agent's context on demand&lt;/td&gt;
&lt;td&gt;Reusable procedures, checklists, playbooks — see &lt;a href="https://www.glukhov.org/ai-devtools/claude-code/claude-skills-for-developers/" rel="noopener noreferrer"&gt;Claude Skills for developers&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subagent&lt;/td&gt;
&lt;td&gt;A separate agent with its own context window, dispatched for delegated work&lt;/td&gt;
&lt;td&gt;Noisy exploration, parallelizable research, anything you want kept &lt;em&gt;out&lt;/em&gt; of the main session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;An external tool/data connector exposed over a protocol&lt;/td&gt;
&lt;td&gt;Reaching systems outside the local session — APIs, databases, remote services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful rule of thumb: a hook enforces a hard constraint deterministically, a Skill gives the main agent a capability inline, and a subagent is for work you want to delegate and keep out of the main context entirely. If a Skill's job is to orchestrate a tool that doesn't exist yet, that is usually a sign you need an MCP server, not a subagent. Claude Code isn't alone in this shape — OpenCode's ecosystem has a comparable idea in its &lt;a href="https://www.glukhov.org/ai-devtools/opencode/oh-my-opencode-agents/" rel="noopener noreferrer"&gt;specialised agents&lt;/a&gt;, which split planning, research, and review across dedicated roles in a similar way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a subagent actually is
&lt;/h2&gt;

&lt;p&gt;Three properties define a Claude Code subagent, and all three matter for how you use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolated context.&lt;/strong&gt; A subagent starts with a fresh window. It does not see your conversation history unless you explicitly fork it, which keeps its output from being polluted by whatever you discussed three turns ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A restricted tool allowlist.&lt;/strong&gt; Subagents can only use a subset of what the parent session already has — they cannot grant themselves new capabilities, and a well-designed subagent should get only the tools its job requires (read-only tools for a research agent, for example).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cross-subagent visibility.&lt;/strong&gt; Subagents cannot see each other's work in progress. If task B genuinely needs task A's output, that is a sequential dependency, not something you can parallelize across two subagents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trigger for reaching for one is not "this task is hard." It is "this task is noisy" — the kind of work that generates a lot of intermediate output (dozens of file reads, a long log, an exploratory grep across the whole repo) where none of that intermediate material needs to survive into your next conversation turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use a subagent (and when not to)
&lt;/h2&gt;

&lt;p&gt;Good fits: codebase exploration before a big change, automated test runs where you only care about pass/fail and failure summaries, security or style reviews, and any multi-step research task whose raw output would otherwise flood your main session.&lt;/p&gt;

&lt;p&gt;Bad fits: two-second lookups ("what does this function return"), anything requiring tight back-and-forth refinement, and dependent tasks you're tempted to "parallelize" even though the second one needs the first one's answer. Using a subagent for a trivial lookup just adds the overhead of spinning up a fresh context window for no real isolation benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the payoff: context and cost math
&lt;/h2&gt;

&lt;p&gt;The pitch for subagents is abstract until you put numbers on a real task. Take a common one: grep a ~500-file service for every place a deprecated config key is still read, then report the exact file:line matches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Main-session context consumed&lt;/th&gt;
&lt;th&gt;What survives into your next turn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct exploration, no subagent&lt;/td&gt;
&lt;td&gt;~35-45K tokens — every grep hit, every file you opened to double-check, every dead end&lt;/td&gt;
&lt;td&gt;All of it, including the wrong turns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegated to an Explore subagent&lt;/td&gt;
&lt;td&gt;~1.5-3K tokens — one summarized report&lt;/td&gt;
&lt;td&gt;Only the findings that mattered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly a 15-20x reduction in what your main session has to carry for that step, which is the actual mechanism behind "subagents keep sessions faster" — it is not magic, it is context that never gets loaded in the first place.&lt;/p&gt;

&lt;p&gt;The cost side compounds the same way. Using the pricing from the &lt;a href="https://www.glukhov.org/ai-devtools/claude-code/" rel="noopener noreferrer"&gt;Claude Code pricing breakdown&lt;/a&gt;, running that same exploration pass on Opus (\$5/MTok input, \$25/MTok output) costs roughly \$0.20-0.25 for the ~40K input tokens alone. Routing it to Haiku (\$1/MTok input, \$5/MTok output) drops that to \$0.04-0.05 — and the main session's Opus budget is never touched by the exploration tokens at all, since it only ever sees the ~2K-token summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining a custom subagent
&lt;/h2&gt;

&lt;p&gt;Custom subagents live as Markdown files with YAML frontmatter, either project-scoped in &lt;code&gt;.claude/agents/&lt;/code&gt; (committed to the repo, shared by the whole team) or user-scoped in &lt;code&gt;~/.claude/agents/&lt;/code&gt; (personal tools you bring to every project).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-reviewer&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;Reviews staged changes for bugs, security issues, and style violations&lt;/span&gt;
  &lt;span class="s"&gt;before commit. Use when the user asks to review, audit, or check&lt;/span&gt;
  &lt;span class="s"&gt;changes prior to committing or opening a PR.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Glob&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonnet&lt;/span&gt;
&lt;span class="na"&gt;skills&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;security-checklist&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="s"&gt;You are a careful code reviewer. Read the staged diff, flag concrete&lt;/span&gt;
&lt;span class="s"&gt;issues with file:line references, and end with a short pass/fail summary.&lt;/span&gt;
&lt;span class="s"&gt;Do not modify any files.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; field is the most important line in the file. It is what the parent session's routing logic reads to decide whether this subagent fits the current task. Write it like a job posting — name the trigger condition explicitly, not a vague "helps with code." Vague descriptions get skipped or misapplied by automatic dispatch.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tools&lt;/code&gt; field is your isolation boundary. Give a research subagent &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Grep&lt;/code&gt;, and &lt;code&gt;Glob&lt;/code&gt; and nothing else; giving it every tool available defeats the entire point of running it in a restricted sandbox. The optional &lt;code&gt;skills&lt;/code&gt; field preloads the full content of named Skills into the subagent's startup context — useful when a subagent needs domain knowledge without spending a turn discovering and loading it mid-task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model routing: cheap models for grunt work
&lt;/h2&gt;

&lt;p&gt;Subagents are also where cost control gets real. Route file discovery, log scanning, and other cheap-to-verify work to Haiku, and reserve Sonnet or Opus for the reasoning-heavy steps — architecture decisions, ambiguous debugging, anything where getting it wrong is expensive. Haiku is roughly 15x cheaper per token than Opus, and on the kind of noisy exploration subagents are built for, that gap adds up fast across a real working session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Explore, Plan, Execute pattern
&lt;/h2&gt;

&lt;p&gt;For complex, multi-step work, the pattern that holds up in practice is Explore, Plan, Execute — using cheap subagents for the parts that generate noise, and keeping the human review gate in the one place it actually matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant You
    participant Main as Main session
    participant Explore as Explore subagent (Haiku)
    participant Execute as Execute agent (Sonnet/Opus)

    You-&amp;gt;&amp;gt;Main: Describe the task
    Main-&amp;gt;&amp;gt;Explore: Delegate codebase exploration
    Explore--&amp;gt;&amp;gt;Main: Return summarized findings
    Main-&amp;gt;&amp;gt;Main: Enter Plan mode, propose approach
    Main-&amp;gt;&amp;gt;You: Show plan for review
    You-&amp;gt;&amp;gt;Main: Approve or adjust
    Main-&amp;gt;&amp;gt;Execute: Hand off approved plan
    Execute--&amp;gt;&amp;gt;Main: Apply changes, run tests
    Main--&amp;gt;&amp;gt;You: Report results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail people get backwards is where the review gate belongs. Exploration is cheap, so let a subagent read freely without asking permission first. Planning is analytical, so let the agent design the approach on its own. But before any agent modifies files, you want to see the plan and approve it — that is what Claude Code's plan mode (&lt;code&gt;permissionMode: plan&lt;/code&gt;) is for, and it is the same principle discussed in the broader &lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;vibe coding best practices&lt;/a&gt; around reviewing every diff before it lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;A handful of mistakes show up repeatedly once teams start writing custom subagents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vague descriptions.&lt;/strong&gt; "Helps with code" will never route correctly. Name the exact trigger condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-broad tool access.&lt;/strong&gt; Giving a read-only research subagent write and bash access removes the isolation guarantee that made it worth creating in the first place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelizing dependent tasks.&lt;/strong&gt; If task B needs task A's finished output, run them sequentially — subagents cannot coordinate mid-task the way a shared orchestrator can. For workflows that genuinely need agents talking to each other mid-task, that's a different shape of problem; see &lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;multi-agent orchestration patterns&lt;/a&gt; if you're building a production system rather than a single-repo workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using a subagent for trivial work.&lt;/strong&gt; "Format this JSON" or "run this one command" doesn't need a fresh context window; just do it directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Worked example: a code-review subagent end to end
&lt;/h2&gt;

&lt;p&gt;Say you want every non-trivial commit reviewed before it lands. Drop the &lt;code&gt;code-reviewer&lt;/code&gt; definition shown earlier into &lt;code&gt;.claude/agents/code-reviewer.md&lt;/code&gt;, commit it so the whole team shares the same reviewer, and invoke it with a natural request like "review my staged changes before I commit." Claude Code matches your request against the subagent's &lt;code&gt;description&lt;/code&gt;, spins it up with only &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Grep&lt;/code&gt;, and &lt;code&gt;Glob&lt;/code&gt; access, and it comes back with file:line-referenced findings and a pass/fail summary — none of the file-by-file noise from getting there ever touches your main session.&lt;/p&gt;

&lt;p&gt;What that looks like in the main transcript, annotated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You:  review my staged changes before I commit

Main: [dispatches code-reviewer subagent — 6 files read, 1 grep pass,
       zero of it shown here]

Main: code-reviewer findings:
      - auth/session.go:142 — token refresh path doesn't handle expired
        refresh token; falls through to nil dereference
      - auth/session.go:203 — style: error wrapped without %w
      PASS/FAIL: FAIL (1 blocking issue)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six file reads and a grep pass happened, and your main session paid for exactly four lines of it. That gap — everything the subagent did versus the three-line summary you actually see — is the entire value proposition in one transcript.&lt;/p&gt;

&lt;p&gt;If your team also uses Spec-Driven Development scaffolds, a review subagent slots naturally into the validation step; see &lt;a href="https://www.glukhov.org/ai-devtools/ai-coding-assistants/spec-kit-vs-kiro-vs-claude-code/" rel="noopener noreferrer"&gt;GitHub Spec Kit vs Kiro vs Claude Code SDD Workflows&lt;/a&gt; for how that review gate compares across portable and IDE-integrated SDD setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth setting up custom subagents?
&lt;/h2&gt;

&lt;p&gt;Not on day one. The built-in general-purpose subagent already covers most exploration and research delegation without you writing a single YAML file, and a single Explore-Plan-Execute pass is enough for most day-to-day work. Write a custom &lt;code&gt;.claude/agents/*.md&lt;/code&gt; file only once you've delegated the &lt;em&gt;same&lt;/em&gt; task by hand three times — a code reviewer, a test-runner triager, a docs-lookup agent for one specific internal library. Teams that write five subagents in their first week usually end up with five stale &lt;code&gt;description&lt;/code&gt; fields nobody updates when the actual trigger condition drifts, which quietly breaks automatic routing months later. Start with zero custom subagents, add one at a time, and only when repetition — not theoretical usefulness — demands it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;p&gt;A few rough edges are worth knowing before you build around subagents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No recursive delegation.&lt;/strong&gt; A subagent cannot spawn its own subagents. If a task genuinely needs a second layer of delegation, that is a sign you want a different orchestration shape — see &lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;multi-agent orchestration patterns&lt;/a&gt; for what that looks like outside a single Claude Code session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No memory across invocations.&lt;/strong&gt; Every dispatch starts from zero, even if you called the same subagent five minutes ago on a related task. There is no built-in mechanism for a subagent to remember its last run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation is a tool allowlist, not a sandbox.&lt;/strong&gt; A subagent with &lt;code&gt;Bash&lt;/code&gt; access can still touch the filesystem and network like any other tool call. Restricting &lt;code&gt;tools&lt;/code&gt; reduces blast radius; it does not create a hard security boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Subagent never triggers.&lt;/strong&gt; The description is almost always the problem. Rewrite it around the specific trigger condition instead of a general capability statement, and double-check the file lives in &lt;code&gt;.claude/agents/&lt;/code&gt; (project) or &lt;code&gt;~/.claude/agents/&lt;/code&gt; (personal) with the right extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subagent burns too much context anyway.&lt;/strong&gt; Check the &lt;code&gt;tools&lt;/code&gt; allowlist — an overly broad toolset invites overly broad exploration. Also check whether the task should have been split into two subagents instead of one doing everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A listed skill doesn't load inside the subagent.&lt;/strong&gt; Claude Code skips a missing or disabled skill named in the &lt;code&gt;skills&lt;/code&gt; field rather than failing the run, and logs a line to that effect in the debug output (&lt;code&gt;/debug&lt;/code&gt; from the main session, then reproduce the dispatch) — something like &lt;code&gt;skill "security-checklist" not found, skipping&lt;/code&gt;. Run &lt;code&gt;/doctor&lt;/code&gt; afterward to confirm the rest of your setup is healthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results feel inconsistent between runs.&lt;/strong&gt; This is often a model-routing issue, not a subagent-design issue — reasoning-heavy work assigned to a cheap model will vary more. Move it to Sonnet or Opus and keep Haiku for the deterministic, low-ambiguity steps.&lt;/p&gt;

&lt;p&gt;Subagents are one piece of a much larger toolbox; if you're comparing Claude Code against the rest of the &lt;a href="https://www.glukhov.org/ai-devtools/" rel="noopener noreferrer"&gt;AI developer tools ecosystem&lt;/a&gt; before committing to this workflow, that overview is a good next stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/claude-code/" rel="noopener noreferrer"&gt;Claude Code install and config for Ollama, llama.cpp, pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/claude-code/claude-skills-for-developers/" rel="noopener noreferrer"&gt;Claude Skills and SKILL.md for Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/ai-coding-assistants/spec-kit-vs-kiro-vs-claude-code/" rel="noopener noreferrer"&gt;GitHub Spec Kit vs Kiro vs Claude Code SDD Workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;What is Vibe Coding?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;Multi-Agent Orchestration Patterns: A Practical Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/opencode/oh-my-opencode-agents/" rel="noopener noreferrer"&gt;Oh My Opencode Specialised Agents Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/" rel="noopener noreferrer"&gt;AI Developer Tools: The Complete Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>aicoding</category>
      <category>dev</category>
    </item>
    <item>
      <title>Ollama to vLLM: When to Migrate Your Local LLM Server</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 02 Aug 2026 13:59:56 +0000</pubDate>
      <link>https://dev.to/rosgluk/ollama-to-vllm-when-to-migrate-your-local-llm-server-26dd</link>
      <guid>https://dev.to/rosgluk/ollama-to-vllm-when-to-migrate-your-local-llm-server-26dd</guid>
      <description>&lt;p&gt;Ollama is one of the easiest ways to run a local language model, but convenience can conceal the moment when a local experiment becomes a shared inference service that needs better scheduling and observability.&lt;/p&gt;

&lt;p&gt;That is where vLLM becomes relevant. Migrating from Ollama to vLLM is not an automatic upgrade, however. It is a trade: you exchange some of Ollama's simplicity for greater control over batching, memory management, concurrency, distributed inference, and production operations.&lt;/p&gt;

&lt;p&gt;This guide covers the practical signals that indicate migration is warranted, the risks of moving too early, and a staged approach that keeps both servers running side by side during validation. The goal is to help you decide based on measurements rather than feature lists. For the wider landscape of local, self-hosted, and cloud options beyond just these two runtimes, see &lt;a href="https://www.glukhov.org/llm-hosting/" rel="noopener noreferrer"&gt;LLM Hosting in 2026: Local, Self-Hosted &amp;amp; Cloud Infrastructure Compared&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama and vLLM Solve Different Problems
&lt;/h2&gt;

&lt;p&gt;Ollama is primarily optimized for convenient model consumption. It gives developers a &lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-cheatsheet/" rel="noopener noreferrer"&gt;concise command-line interface&lt;/a&gt;, a local API, a model library, Modelfiles, and straightforward support for common desktop and workstation configurations.&lt;/p&gt;

&lt;p&gt;vLLM is an inference engine and serving platform. Its central concerns are high-throughput request scheduling, efficient KV cache management, continuous batching, model parallelism, and compatibility with applications built for OpenAI-style APIs.&lt;/p&gt;

&lt;p&gt;The distinction matters because the two servers can look similar from the outside. Both can expose a chat API, stream tokens, run quantized models, and serve local applications. Their operating models become visibly different only when the server is placed under sustained or concurrent load.&lt;/p&gt;

&lt;p&gt;A useful summary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Ollama&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast local setup&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;More involved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curated model downloads&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Usually Hugging Face based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GGUF workflow&lt;/td&gt;
&lt;td&gt;First-class&lt;/td&gt;
&lt;td&gt;Supported, not main strength&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-user chat&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Often unnecessary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent API traffic&lt;/td&gt;
&lt;td&gt;Limited but configurable&lt;/td&gt;
&lt;td&gt;Core use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous batching&lt;/td&gt;
&lt;td&gt;Not the primary model&lt;/td&gt;
&lt;td&gt;Core feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix cache reuse&lt;/td&gt;
&lt;td&gt;Limited operational control&lt;/td&gt;
&lt;td&gt;Built-in optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-GPU model serving&lt;/td&gt;
&lt;td&gt;Limited compared with vLLM&lt;/td&gt;
&lt;td&gt;Tensor and pipeline parallelism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production metrics&lt;/td&gt;
&lt;td&gt;Basic response timing data&lt;/td&gt;
&lt;td&gt;Prometheus metrics endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment tuning&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Extensive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The question is not which server is universally better. It is whether your workload still matches the operating model that makes Ollama attractive. If you want the fuller picture across more than these two runtimes, &lt;a href="https://www.glukhov.org/llm-hosting/comparisons/hosting-llms-ollama-localai-jan-lmstudio-vllm-comparison/" rel="noopener noreferrer"&gt;our comparison of Ollama, vLLM, LocalAI, Jan, LM Studio and other local LLM tools&lt;/a&gt; covers the broader field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs You Have Outgrown Ollama
&lt;/h2&gt;

&lt;p&gt;A slow response does not, by itself, justify a migration. Generation speed is often constrained by model size, quantization, memory bandwidth, prompt length, or GPU capability rather than the serving engine, and the stronger migration signals only appear once workload shape itself starts to matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple Users Cause Unstable Latency
&lt;/h3&gt;

&lt;p&gt;A local LLM server can feel fast during an isolated test and then degrade sharply when several clients connect. Requests begin waiting behind long generations, time to first token becomes inconsistent, and a single large prompt can affect everyone sharing the model.&lt;/p&gt;

&lt;p&gt;Ollama can process parallel requests, and &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; controls how many requests a loaded model may handle concurrently — see &lt;a href="https://www.glukhov.org/llm-performance/ollama/how-ollama-handles-parallel-requests/" rel="noopener noreferrer"&gt;how Ollama handles parallel requests&lt;/a&gt; for the queuing and memory mechanics behind that setting. That parallelism is not free: memory requirements grow with both the configured parallel request count and context length.&lt;/p&gt;

&lt;p&gt;This is often the first practical warning. A configuration that works for one 8K conversation may become impossible when four clients each reserve a much larger context.&lt;/p&gt;

&lt;p&gt;vLLM is designed to combine work from active requests through continuous batching. Instead of treating each request as an isolated inference job, it continuously updates the batch as sequences arrive, generate tokens, and finish — a scheduling model that generally becomes more valuable as concurrency increases.&lt;/p&gt;

&lt;h3&gt;
  
  
  GPU Utilization Is Low While Requests Are Queued
&lt;/h3&gt;

&lt;p&gt;A queue does not necessarily mean that the GPU is fully used. In a simple serving arrangement, work may be serialized even though additional requests could have contributed useful computation to the current decode step.&lt;/p&gt;

&lt;p&gt;vLLM's scheduler is designed to keep more useful work in flight. PagedAttention manages KV cache memory in blocks, while continuous batching allows active sequences to enter and leave the execution batch dynamically.&lt;/p&gt;

&lt;p&gt;The result is not guaranteed to be lower latency for every individual request. Under load, however, it can produce substantially better aggregate throughput and more predictable resource utilization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Long Prompts Dominate Time to First Token
&lt;/h3&gt;

&lt;p&gt;Long-context coding assistants, RAG pipelines, and agent sessions can repeatedly send large system prompts or shared document prefixes. Processing those input tokens is the prefill stage, and it can dominate time to first token.&lt;/p&gt;

&lt;p&gt;vLLM supports chunked prefill and automatic prefix caching. Prefix caching allows later requests to reuse KV cache blocks when their initial token sequence matches an already processed prefix.&lt;/p&gt;

&lt;p&gt;This is particularly useful when requests share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A long system prompt&lt;/li&gt;
&lt;li&gt;The same tool definitions&lt;/li&gt;
&lt;li&gt;A stable repository summary&lt;/li&gt;
&lt;li&gt;Repeated few-shot examples&lt;/li&gt;
&lt;li&gt;A common RAG document prefix&lt;/li&gt;
&lt;li&gt;A shared conversation history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefix caching does not make output generation faster. It reduces repeated prompt computation, so its benefit depends on whether requests actually contain identical reusable prefixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  You Need More Than One GPU
&lt;/h3&gt;

&lt;p&gt;A model that does not fit on one GPU is a strong reason to consider vLLM. It supports tensor parallelism across GPUs and pipeline parallelism across multiple nodes or devices.&lt;/p&gt;

&lt;p&gt;This does not make multi-GPU inference effortless. GPU interconnect bandwidth, PCIe topology, model architecture, container shared memory, and communication overhead still affect performance.&lt;/p&gt;

&lt;p&gt;Nevertheless, vLLM provides a deliberate path for distributed inference. Ollama is usually a better match for a single desktop or workstation where the chosen model already fits comfortably.&lt;/p&gt;

&lt;h3&gt;
  
  
  You Need Production-Level Observability
&lt;/h3&gt;

&lt;p&gt;Ollama API responses expose useful timing fields such as model load duration, prompt evaluation duration, generated token count, and generation duration. These values are enough for local benchmarking and application-level logging.&lt;/p&gt;

&lt;p&gt;vLLM exposes Prometheus-compatible metrics through its &lt;code&gt;/metrics&lt;/code&gt; endpoint. That makes it easier to track request volume, queueing, time to first token, inter-token latency, cache usage, preemptions, throughput, and request outcomes over time.&lt;/p&gt;

&lt;p&gt;Once users depend on the service, observability stops being optional. Without queue, cache, and latency metrics, it is difficult to distinguish an undersized GPU from an oversized context limit, poor scheduling, cold model loading, or simply too many simultaneous requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where vLLM Actually Wins
&lt;/h2&gt;

&lt;p&gt;vLLM's most important advantage is not that it can produce one response faster than Ollama on every machine. The meaningful advantage is that it gives the operator more mechanisms for using expensive accelerator memory and compute efficiently across many requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Continuous Batching
&lt;/h3&gt;

&lt;p&gt;Traditional static batching works best when requests have similar input and output lengths. Interactive LLM traffic rarely behaves that way: one user asks for a short classification, another submits a 20K-token prompt, and a third generates several thousand tokens of code.&lt;/p&gt;

&lt;p&gt;Continuous batching changes the active batch as requests progress. Completed sequences leave, new sequences enter, and the engine attempts to avoid wasting batch capacity on requests that have already finished.&lt;/p&gt;

&lt;p&gt;This improves throughput when traffic is concurrent and uneven. It provides little benefit when a single user sends one request at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paged KV Cache Management
&lt;/h3&gt;

&lt;p&gt;During generation, the server stores attention keys and values for previously processed tokens. This KV cache can consume a large amount of GPU memory, especially with long contexts and multiple active sequences.&lt;/p&gt;

&lt;p&gt;vLLM manages this cache in blocks instead of requiring each sequence to reserve one large contiguous allocation. The approach reduces memory fragmentation and allows available cache capacity to be used more flexibly.&lt;/p&gt;

&lt;p&gt;The practical value is higher concurrency within the same memory budget. It does not remove the underlying cost of long context, but it reduces avoidable waste around that cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefix Caching
&lt;/h3&gt;

&lt;p&gt;Many production requests share a substantial beginning. Tool-enabled agents may send identical function schemas, support bots may use the same policy documents, and coding assistants may repeatedly include the same repository instructions.&lt;/p&gt;

&lt;p&gt;Automatic prefix caching can reuse the computed cache for matching prefixes. It is especially useful when a stable, large prefix is followed by a relatively small request-specific suffix.&lt;/p&gt;

&lt;p&gt;It is less useful when templates, timestamps, document ordering, or dynamically generated metadata change near the beginning of every prompt. Small differences in tokenization can prevent the prefix from matching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel and Distributed Inference
&lt;/h3&gt;

&lt;p&gt;vLLM supports several forms of parallelism, including tensor, pipeline, data, expert, and context parallelism. Not every deployment needs these modes, but their availability matters when a service grows beyond one GPU.&lt;/p&gt;

&lt;p&gt;For a workstation with two suitable GPUs, tensor parallelism may allow a larger model to run across both devices. For a replicated service, data parallelism can create multiple engine replicas for additional throughput.&lt;/p&gt;

&lt;p&gt;These features introduce operational complexity. They should be adopted because measurements demonstrate a capacity problem, not because distributed inference appears more sophisticated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broader Production Controls
&lt;/h3&gt;

&lt;p&gt;vLLM exposes controls for GPU memory utilization, maximum model length, maximum active sequences, quantization, cache data types, &lt;a href="https://www.glukhov.org/llm-performance/optimization/speculative-decoding/" rel="noopener noreferrer"&gt;speculative decoding&lt;/a&gt;, tool calling, structured output, model aliases, authentication keys, and distributed execution.&lt;/p&gt;

&lt;p&gt;That flexibility makes the server easier to tune for a particular workload, but it also creates more opportunities for an invalid or inefficient configuration. Migrating to vLLM means taking responsibility for those decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Ollama Still Wins
&lt;/h2&gt;

&lt;p&gt;A migration guide should not treat Ollama as an inferior preliminary tool. For many local deployments, it remains the better server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Personal Workstations
&lt;/h3&gt;

&lt;p&gt;For one developer using a chat interface, code assistant, or occasional local API, the operational advantages of vLLM may never compensate for its additional setup.&lt;/p&gt;

&lt;p&gt;Ollama installs quickly, downloads models through a simple registry, and hides many model-specific details. It is well suited to experimentation and private desktop use.&lt;/p&gt;

&lt;h3&gt;
  
  
  GGUF Model Collections
&lt;/h3&gt;

&lt;p&gt;Ollama has a natural workflow around GGUF models and Modelfiles. Existing users may have curated quantizations, adapters, templates, system prompts, and parameters that work reliably with their hardware.&lt;/p&gt;

&lt;p&gt;vLLM supports GGUF, but its strongest path is generally through supported Hugging Face model repositories and quantization formats such as AWQ, GPTQ, BitsAndBytes, FP8, or vendor-specific formats. Moving an existing GGUF deployment to vLLM without evaluating a more native checkpoint format can preserve the inconvenience of migration while missing some of the performance advantages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mixed CPU and GPU Offloading
&lt;/h3&gt;

&lt;p&gt;Desktop inference sometimes relies on partial GPU offloading because the entire model does not fit in VRAM. This can be practical for occasional use, particularly when latency is not critical.&lt;/p&gt;

&lt;p&gt;vLLM is generally most compelling when the model and required KV cache capacity can be served effectively by the available accelerator configuration. A workload that depends heavily on system RAM and CPU offloading may be better suited to Ollama or llama.cpp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rapid Model Switching
&lt;/h3&gt;

&lt;p&gt;Ollama makes it easy to pull, run, stop, and switch among many local models. That is useful for evaluation, writing, coding, embeddings, vision, and ad hoc experimentation.&lt;/p&gt;

&lt;p&gt;A vLLM deployment is more commonly built around a deliberately selected model that remains loaded as a service. Multi-model deployment is possible, but it requires more explicit resource planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal Administration
&lt;/h3&gt;

&lt;p&gt;Ollama is intentionally opinionated. That can be a limitation under load, but it is an advantage when nobody wants to maintain an inference platform, and if the local server has one user, acceptable latency, and no meaningful queue, migration is likely to create work rather than remove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Migrate Based on Tokens per Second Alone
&lt;/h2&gt;

&lt;p&gt;Single-request token generation speed is an incomplete benchmark. Two servers may produce similar decode throughput for one sequence while behaving very differently with eight concurrent clients.&lt;/p&gt;

&lt;p&gt;A useful evaluation should measure at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to first token&lt;/li&gt;
&lt;li&gt;Inter-token latency&lt;/li&gt;
&lt;li&gt;End-to-end request latency&lt;/li&gt;
&lt;li&gt;Prompt processing throughput&lt;/li&gt;
&lt;li&gt;Output token throughput&lt;/li&gt;
&lt;li&gt;Requests completed per minute&lt;/li&gt;
&lt;li&gt;Queue wait time&lt;/li&gt;
&lt;li&gt;GPU memory consumption&lt;/li&gt;
&lt;li&gt;GPU utilization&lt;/li&gt;
&lt;li&gt;Failure and timeout rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the same model family, precision, context length, prompt set, output limit, and concurrency level on both servers. Otherwise, the test is more likely to compare model packaging and configuration than serving engines.&lt;/p&gt;

&lt;p&gt;The most useful comparison is a small load test that represents your real traffic. For a shared coding assistant, that might include long system prompts, repeated prefixes, streaming responses, and two to eight simultaneous sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan the Model Migration First
&lt;/h2&gt;

&lt;p&gt;Ollama model names do not automatically map to equivalent vLLM model identifiers. An Ollama package may contain a particular GGUF quantization, prompt template, stop-token configuration, and default parameters.&lt;/p&gt;

&lt;p&gt;Before changing the server, identify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The original model family and version&lt;/li&gt;
&lt;li&gt;Whether it is a base or instruction-tuned model&lt;/li&gt;
&lt;li&gt;The current quantization and effective precision&lt;/li&gt;
&lt;li&gt;The prompt or chat template&lt;/li&gt;
&lt;li&gt;The configured context length&lt;/li&gt;
&lt;li&gt;Stop tokens and generation defaults&lt;/li&gt;
&lt;li&gt;Tool-calling or structured-output requirements&lt;/li&gt;
&lt;li&gt;Any LoRA adapters or custom system prompts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then choose a vLLM-supported checkpoint that matches the intended behavior. Do not assume that an AWQ or FP8 checkpoint will behave identically to the GGUF build previously used in Ollama — the model migration is often more significant than the API migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check VRAM Before Starting vLLM
&lt;/h2&gt;

&lt;p&gt;A model fitting into GPU memory does not mean that it can serve the required workload. VRAM must cover more than model weights.&lt;/p&gt;

&lt;p&gt;The practical memory budget includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model weights
+ KV cache
+ CUDA graphs and runtime allocations
+ temporary workspace
+ multimodal processor caches, if used
+ safety margin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long contexts and concurrent sequences primarily expand the KV cache requirement. Increasing the maximum context length therefore reduces the number of simultaneous requests that can fit, even if most requests never use the full limit.&lt;/p&gt;

&lt;p&gt;Start with a realistic &lt;code&gt;--max-model-len&lt;/code&gt; rather than the largest value advertised by the model, and avoid setting GPU memory utilization so aggressively that minor workload variation causes out-of-memory failures. A stable service with slightly less theoretical capacity is more useful than one that fails at its first traffic spike.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal vLLM Docker Compose Deployment
&lt;/h2&gt;

&lt;p&gt;The following example starts an OpenAI-compatible vLLM server on port 8000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vllm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm/vllm-openai:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;ipc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;gpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${HOME}/.cache/huggingface:/root/.cache/huggingface&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;HF_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${HF_TOKEN:-}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--model&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Qwen/Qwen3-8B&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--served-model-name&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;local-model&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--max-model-len&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16384"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--gpu-memory-utilization&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.90"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--api-key&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${VLLM_API_KEY:-change-me}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
HF_TOKEN=
VLLM_API_KEY=replace-with-a-long-random-value
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; vllm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the models endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/v1/models &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer replace-with-a-long-random-value"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send a chat request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer replace-with-a-long-random-value"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "local-model",
    "messages": [
      {
        "role": "user",
        "content": "Explain continuous batching in two paragraphs."
      }
    ],
    "temperature": 0.2,
    "max_tokens": 300,
    "stream": false
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a maintained deployment, pin the image to a tested vLLM release instead of leaving it on &lt;code&gt;latest&lt;/code&gt;. Review release notes before upgrading because command-line options, model implementations, metrics, and engine behavior can evolve. This Compose file is intentionally minimal; for the fuller setup guide — OpenAI API compatibility, PagedAttention tuning, and a deeper vLLM-vs-Ollama comparison — see the &lt;a href="https://www.glukhov.org/llm-hosting/vllm/vllm-quickstart/" rel="noopener noreferrer"&gt;vLLM Quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAI API Compatibility Is Not Complete Interchangeability
&lt;/h2&gt;

&lt;p&gt;Both Ollama and vLLM provide OpenAI-compatible endpoints, which can make the application migration relatively small. In many clients, changing the base URL, API key, and model name is enough to establish a connection.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace-with-a-long-random-value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What should I monitor on an LLM server?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compatibility should still be tested at the feature level. Examine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming event behavior&lt;/li&gt;
&lt;li&gt;Supported request parameters&lt;/li&gt;
&lt;li&gt;Chat template selection&lt;/li&gt;
&lt;li&gt;Tool-call parsing&lt;/li&gt;
&lt;li&gt;Reasoning output handling&lt;/li&gt;
&lt;li&gt;JSON or schema-constrained output&lt;/li&gt;
&lt;li&gt;Embeddings endpoints&lt;/li&gt;
&lt;li&gt;Multimodal inputs&lt;/li&gt;
&lt;li&gt;Token usage reporting&lt;/li&gt;
&lt;li&gt;Error response formats&lt;/li&gt;
&lt;li&gt;Model name discovery&lt;/li&gt;
&lt;li&gt;Context-length enforcement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A client that only sends ordinary chat completions will usually be easier to migrate than an agent framework that depends on a particular tool-call parser or nonstandard extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chat Templates Are a Common Migration Failure
&lt;/h2&gt;

&lt;p&gt;Instruction-tuned models expect conversations to be serialized using a specific chat template. The template inserts role markers, separators, control tokens, and generation prompts in the format used during training.&lt;/p&gt;

&lt;p&gt;Ollama packages much of this behavior inside its model definition. With vLLM, the template is normally obtained from the model tokenizer configuration, although an operator can provide one explicitly.&lt;/p&gt;

&lt;p&gt;A server may start successfully even when the selected template is wrong. The symptoms appear in model behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model repeats role labels&lt;/li&gt;
&lt;li&gt;Responses contain special tokens&lt;/li&gt;
&lt;li&gt;System instructions are ignored&lt;/li&gt;
&lt;li&gt;Tool calls are malformed&lt;/li&gt;
&lt;li&gt;The model continues the user message&lt;/li&gt;
&lt;li&gt;Output quality is much worse than expected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before blaming the inference engine, compare the fully rendered prompt used by each deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a Staged Migration
&lt;/h2&gt;

&lt;p&gt;Replacing a working local server in one step creates unnecessary risk. Ollama and vLLM can run side by side on different ports while you validate the new deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Reproduce One Model
&lt;/h3&gt;

&lt;p&gt;Choose the model responsible for most API traffic and match its instruction tuning, context requirement, generation parameters, and chat behavior as closely as possible. Do not begin by moving every experimental model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Validate API Behavior
&lt;/h3&gt;

&lt;p&gt;Run existing integration tests against the vLLM endpoint, including streaming, cancellation, timeouts, tool calls, malformed requests, context overflow, and concurrent access. Record behavioral differences rather than hiding them behind client retries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Establish a Baseline
&lt;/h3&gt;

&lt;p&gt;Measure one-request performance first. This confirms that the model is loaded correctly and provides a reference for later tests.&lt;/p&gt;

&lt;p&gt;Record prompt tokens per second, output tokens per second, time to first token, total latency, and GPU memory usage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4: Add Realistic Concurrency
&lt;/h3&gt;

&lt;p&gt;Test the number of simultaneous requests expected in normal operation and during a plausible peak, using representative prompt and output lengths rather than identical synthetic requests. Watch queueing, cache use, preemptions, time to first token, and tail latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 5: Move One Client
&lt;/h3&gt;

&lt;p&gt;Route a noncritical application or a small percentage of traffic to vLLM. Keep Ollama available as a fallback until the new server has operated reliably under real use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 6: Tune From Measurements
&lt;/h3&gt;

&lt;p&gt;Adjust model length, memory utilization, maximum active sequences, prefix caching, parallelism, and quantization only after identifying a measured constraint. Changing several parameters at once makes performance regressions difficult to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Before switching clients, verify the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] The target model is supported by vLLM
[ ] The selected checkpoint and quantization fit in VRAM
[ ] Enough VRAM remains for the required KV cache
[ ] The maximum context length reflects real usage
[ ] The correct chat template is available
[ ] Stop tokens and generation defaults are tested
[ ] Streaming works with existing clients
[ ] Tool calls and structured output are validated
[ ] The public model alias remains stable
[ ] Authentication is enabled
[ ] The server is not exposed directly to the internet
[ ] Prometheus metrics are collected
[ ] GPU metrics are collected separately
[ ] Load tests include realistic concurrency
[ ] Timeouts and cancellations are handled
[ ] A rollback path to Ollama exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This list is deliberately operational. Installing vLLM is usually easier than proving that it behaves correctly for an existing application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Network Exposure
&lt;/h2&gt;

&lt;p&gt;Neither a local Ollama endpoint nor a vLLM endpoint should be casually exposed to the public internet. An unauthenticated inference server can consume expensive GPU capacity, reveal model behavior, and become a route for denial-of-service attacks through very long prompts or outputs.&lt;/p&gt;

&lt;p&gt;vLLM can require an API key for its OpenAI-compatible endpoints, but an API key is not a complete security boundary. For shared or remote access, place the service behind a reverse proxy or API gateway that provides TLS, network restrictions, request-size limits, rate limits, access logging, and appropriate authentication — the same pattern covered in &lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-behind-reverse-proxy/" rel="noopener noreferrer"&gt;Ollama behind a reverse proxy with Caddy or Nginx&lt;/a&gt; applies just as well in front of vLLM.&lt;/p&gt;

&lt;p&gt;Also consider model-specific risks. Multimodal URL loading, custom model code, remote files, and unrestricted tool execution can expand the attack surface beyond ordinary text generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Migrate
&lt;/h2&gt;

&lt;p&gt;Stay with Ollama when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One or two users access the server&lt;/li&gt;
&lt;li&gt;Requests are mostly sequential&lt;/li&gt;
&lt;li&gt;The model already delivers acceptable latency&lt;/li&gt;
&lt;li&gt;Easy GGUF management is important&lt;/li&gt;
&lt;li&gt;CPU or partial GPU offloading is required&lt;/li&gt;
&lt;li&gt;Models are changed frequently&lt;/li&gt;
&lt;li&gt;Nobody wants to operate additional infrastructure&lt;/li&gt;
&lt;li&gt;There is no measured concurrency or throughput problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A move to vLLM should solve a concrete limitation. "Production" is not a magic threshold that invalidates Ollama, especially for an internal service with modest traffic.&lt;/p&gt;

&lt;p&gt;Conversely, do not preserve Ollama merely because it was easier to install. If users regularly wait in a queue, repeated prefixes consume significant prefill time, or a larger model must be distributed across GPUs, the simpler server may have become the more expensive choice operationally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Ollama for Development and Add vLLM for Shared Serving
&lt;/h2&gt;

&lt;p&gt;The most practical architecture is often not a complete replacement. Developers can keep &lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-in-docker-compose/" rel="noopener noreferrer"&gt;Ollama running in Docker Compose&lt;/a&gt; on their workstations for model exploration, GGUF testing, and private interactive use while a shared vLLM instance serves a stable model to applications and teams. That split also matters for &lt;a href="https://www.glukhov.org/llm-hosting/self-hosting/llm-selfhosting-and-ai-sovereignty/" rel="noopener noreferrer"&gt;AI sovereignty&lt;/a&gt; — keeping both runtimes self-hosted means prompts, weights, and inference logs stay under your control regardless of which server handles a given request.&lt;/p&gt;

&lt;p&gt;This separates two different workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ollama:
experimentation -&amp;gt; model switching -&amp;gt; personal tools -&amp;gt; local chat

vLLM:
selected model -&amp;gt; shared endpoint -&amp;gt; concurrent traffic -&amp;gt; monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arrangement also lowers migration risk. Models can be tested locally before a suitable checkpoint is promoted to the shared vLLM deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Decision Flow
&lt;/h2&gt;

&lt;p&gt;The following diagram summarizes the key decision points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Ollama serving LLM] --&amp;gt; B{Multiple users&amp;lt;br&amp;gt;with unstable latency?}
    B --&amp;gt;|No| C[Stay with Ollama]
    B --&amp;gt;|Yes| D{Long shared&amp;lt;br&amp;gt;prefixes?}
    D --&amp;gt;|Yes| E[Strong vLLM signal]
    D --&amp;gt;|No| F{Need multi-GPU&amp;lt;br&amp;gt;or observability?}
    F --&amp;gt;|Yes| E
    F --&amp;gt;|No| G{Measured concurrency&amp;lt;br&amp;gt;problem?}
    G --&amp;gt;|No| C
    G --&amp;gt;|Yes| E
    E --&amp;gt; H[Plan staged migration]
    H --&amp;gt; I[Validate side by side]
    I --&amp;gt; J[Switch clients gradually]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Ollama is difficult to beat as a local model runner. It removes enough packaging and configuration work that developers can concentrate on the model and application rather than the inference stack.&lt;/p&gt;

&lt;p&gt;vLLM becomes the stronger choice when the server itself is the problem to be engineered. Concurrent traffic, queueing, repeated long prefixes, multi-GPU models, capacity planning, and production observability are the migration signals that matter.&lt;/p&gt;

&lt;p&gt;Do not migrate because vLLM has a longer feature list. Migrate when measurements show that Ollama's simpler operating model no longer matches the workload. Until that point, simplicity is not a technical weakness; it is an optimization.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>vllm</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>Keeping Specs, Tests, And Code In Sync In AI Development</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 01 Aug 2026 10:43:20 +0000</pubDate>
      <link>https://dev.to/rosgluk/keeping-specs-tests-and-code-in-sync-in-ai-development-jm4</link>
      <guid>https://dev.to/rosgluk/keeping-specs-tests-and-code-in-sync-in-ai-development-jm4</guid>
      <description>&lt;p&gt;AI coding agents ship features fast, but specs, tests, and code quietly drift apart. This guide covers a traceability model, spec-to-test and spec-to-code mapping, and the CI checks that catch drift before a merge.&lt;/p&gt;

&lt;p&gt;A spec that nobody re-checks against the running system is worse than no spec at all, because it creates false confidence. Reviewers trust the document instead of the diff, and an AI agent asked to "follow the existing pattern" will happily follow whatever the code actually does, even when that contradicts the requirement it was supposed to satisfy.&lt;/p&gt;

&lt;p&gt;The fix is not more documentation. It is a small, enforceable link between four things that already exist in most repositories: the requirement, the design decision behind it, the tests that prove it, and the commits or pull requests that changed it.&lt;/p&gt;

&lt;p&gt;Once that link exists as data rather than as a shared understanding, you can query it. You can ask which requirements have no test coverage, which tests no longer map to any requirement, and which files changed in a pull request without a matching requirement ID. That query is the actual deliverable of this article, and the rest of the post walks through how to build it with tools you likely already run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Drift Problem: Why Specs, Tests, And Code Fall Out Of Sync
&lt;/h2&gt;

&lt;p&gt;Drift shows up in four recognisable shapes, and AI-assisted teams tend to hit all four faster than teams that write every line by hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec changes, code does not.&lt;/strong&gt; A requirement gets clarified in a follow-up conversation or a comment thread, but nobody regenerates or edits the implementation to match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code changes, spec does not.&lt;/strong&gt; An agent or a developer fixes a bug or refactors a module, and the spec keeps describing the old behavior as if it were still current.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests cover implementation, not intent.&lt;/strong&gt; Unit tests assert what the code currently does, which is circular: they pass by construction even when the code satisfies the wrong requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull requests do not reference requirements.&lt;/strong&gt; Reviewers approve a diff on the strength of "looks reasonable" because there is no explicit claim to check it against.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recent process research on AI development frameworks identifies specification drift as a recurring risk precisely because agents regenerate code quickly and repeatedly, and each regeneration is a fresh opportunity for spec and implementation to diverge a little further. The &lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/spec-driven-development-vs-vibe-coding/" rel="noopener noreferrer"&gt;Spec-Driven Development vs Vibe Coding&lt;/a&gt; debate is really an argument about this same failure mode: a spec that nobody enforces degenerates into the same drift you get without one, just with extra ceremony.&lt;/p&gt;

&lt;p&gt;Modern spec-kit-style workflows increasingly frame this as &lt;strong&gt;specification rot&lt;/strong&gt;: the spec keeps looking authoritative while quietly losing its connection to what the system actually does. The &lt;a href="https://www.glukhov.org/app-architecture/documentation/what-is-spec-driven-development/" rel="noopener noreferrer"&gt;core definition of spec-driven development&lt;/a&gt; treats the spec as the source of truth, but a source of truth only stays true if something keeps checking it against reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Traceability Model For AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;A workable traceability model needs six identifiers that connect a business requirement all the way down to the lines of code and the pull request that implemented it. Most teams already have three or four of these; the missing ones are usually the design decision ID and the explicit link back from tests and commits.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Lives in&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirement ID&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requirements.md&lt;/code&gt; or spec tool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REQ-014&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design decision ID&lt;/td&gt;
&lt;td&gt;ADR / decision record&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ADR-0032&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task ID&lt;/td&gt;
&lt;td&gt;task breakdown or issue tracker&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TASK-014-3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test ID&lt;/td&gt;
&lt;td&gt;test file or test name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;test_req_014_password_reset&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit / PR link&lt;/td&gt;
&lt;td&gt;Git history&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PR #482&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed files&lt;/td&gt;
&lt;td&gt;Git diff&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auth/reset.go&lt;/code&gt;, &lt;code&gt;auth/reset_test.go&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The relationships between these identifiers form a graph rather than a straight line, because one requirement can spawn several tasks, and one pull request can touch several requirements at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    REQ["Requirement&amp;lt;br/&amp;gt;REQ-014"] --&amp;gt; ADR["Design Decision&amp;lt;br/&amp;gt;ADR-0032"]
    ADR --&amp;gt; TASK["Task&amp;lt;br/&amp;gt;TASK-014-3"]
    TASK --&amp;gt; CODE["Code Change&amp;lt;br/&amp;gt;auth/reset.go"]
    TASK --&amp;gt; TEST["Test&amp;lt;br/&amp;gt;test_req_014_password_reset"]
    CODE --&amp;gt; PR["Pull Request&amp;lt;br/&amp;gt;#482"]
    TEST --&amp;gt; PR
    PR --&amp;gt; COMMIT["Commit history"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storing this graph as structured data, not prose, is what lets you query it later. GitHub's Spec Kit ecosystem has moved in exactly this direction: extensions like &lt;code&gt;spec-kit-trace&lt;/code&gt; scan &lt;code&gt;REQ-XXX&lt;/code&gt; tokens embedded in spec files and test files and generate a deterministic matrix from that literal text match, deliberately avoiding fuzzy name-based guessing that produces silent false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec-To-Test Mapping: Turning Acceptance Criteria Into Test Cases
&lt;/h2&gt;

&lt;p&gt;Every acceptance criterion in a spec is, by construction, a behavioral assertion: given this state, when the actor does this, then the system should respond that way. That is already the shape of a test case, which is why the strongest SDD workflows generate tests from the same acceptance criteria that generate the code, instead of asking the code-generating agent to also invent its own tests after the fact.&lt;/p&gt;

&lt;p&gt;A widely used format for writing these criteria is EARS (Easy Approach to Requirements Syntax), which forces each requirement into an unambiguous, testable pattern such as "When &amp;lt;trigger&amp;gt;, the system shall &amp;lt;response&amp;gt;." That structure maps cleanly onto four categories of test that every requirement should carry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Positive tests&lt;/strong&gt; — the happy path the requirement explicitly describes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative tests&lt;/strong&gt; — inputs or states the requirement says must be rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundary tests&lt;/strong&gt; — the edges of ranges, limits, and thresholds mentioned in the acceptance criteria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration tests&lt;/strong&gt; — behavior for data or state that predates the requirement, so an old record does not silently bypass a new rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement type&lt;/th&gt;
&lt;th&gt;Test category to add&lt;/th&gt;
&lt;th&gt;Common miss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"System shall reject X"&lt;/td&gt;
&lt;td&gt;Negative&lt;/td&gt;
&lt;td&gt;Only the accept path is tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Limit is N items"&lt;/td&gt;
&lt;td&gt;Boundary&lt;/td&gt;
&lt;td&gt;N-1, N, and N+1 are not all covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"New field replaces old field"&lt;/td&gt;
&lt;td&gt;Migration&lt;/td&gt;
&lt;td&gt;Old records with no new field crash silently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Within 60 seconds"&lt;/td&gt;
&lt;td&gt;Boundary + timing&lt;/td&gt;
&lt;td&gt;Test asserts logic, not the actual time budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unit tests written this way still matter as the fast, cheap layer of the pyramid; the practical patterns for structuring them are covered in the &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-go/" rel="noopener noreferrer"&gt;Go unit testing guide&lt;/a&gt; and the &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-python/" rel="noopener noreferrer"&gt;Python unit testing guide&lt;/a&gt;. What traceability adds on top is a literal, stable requirement token embedded in the test name or a test comment, so a later query can prove — not assume — that &lt;code&gt;REQ-014&lt;/code&gt; has coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec-To-Code Mapping: From Design Plans To A Trace Table
&lt;/h2&gt;

&lt;p&gt;Spec-to-test mapping proves behavior; spec-to-code mapping proves scope. It answers a different question: which files were actually supposed to change for this requirement, and did the diff stay inside that boundary or spill into unrelated modules?&lt;/p&gt;

&lt;p&gt;A design plan that lists affected files up front — even a rough list — gives you something to diff the real pull request against later. Comments in code should only reference a requirement ID when doing so adds information a reviewer cannot get from the spec itself; a comment repeating the requirement text verbatim is noise, but &lt;code&gt;// enforces REQ-014 boundary: max 5 reset attempts per hour&lt;/code&gt; earns its place because the number is otherwise invisible in the diff.&lt;/p&gt;

&lt;p&gt;A generated trace table turns this into something reviewable in seconds rather than something a reviewer has to reconstruct by reading both documents side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Design decision&lt;/th&gt;
&lt;th&gt;Files changed&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REQ-014&lt;/td&gt;
&lt;td&gt;ADR-0032&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auth/reset.go&lt;/code&gt;, &lt;code&gt;auth/reset_test.go&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;test_req_014_*&lt;/code&gt; (4)&lt;/td&gt;
&lt;td&gt;Covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REQ-015&lt;/td&gt;
&lt;td&gt;ADR-0032&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auth/reset.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gap&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REQ-016&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auth/notify.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;test_notify_basic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Orphan spec link&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That single table surfaces two of the most common failure patterns at a glance: &lt;code&gt;REQ-015&lt;/code&gt; changed code with zero matching tests, and the test attached to &lt;code&gt;REQ-016&lt;/code&gt; does not actually reference a requirement ID, which means either the spec is missing or the test was misfiled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pull Request Workflow: Reviewing Spec, Code, And Test Diffs Together
&lt;/h2&gt;

&lt;p&gt;A pull request built around traceability reviews three diffs side by side instead of one: what changed in the spec, what changed in the code, and what changed in the tests. The review question stops being "does this look right?" and becomes the far more specific "which requirement does this change satisfy, and does the evidence prove it?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Dev as Developer or Agent
    participant PR as Pull Request
    participant CI as CI Pipeline
    participant Rev as Reviewer

    Dev-&amp;gt;&amp;gt;PR: Open PR with spec diff + code diff + test diff
    PR-&amp;gt;&amp;gt;CI: Trigger traceability checks
    CI-&amp;gt;&amp;gt;CI: Verify REQ-ID present in PR description
    CI-&amp;gt;&amp;gt;CI: Run spec-to-test coverage check
    CI-&amp;gt;&amp;gt;CI: Run spec-to-code file-scope check
    CI--&amp;gt;&amp;gt;PR: Post trace report as PR comment
    Rev-&amp;gt;&amp;gt;PR: Review against "which requirement does this satisfy?"
    Rev-&amp;gt;&amp;gt;PR: Approve or request changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A short, concrete reviewer checklist works better here than a long one, because reviewers skip long checklists under deadline pressure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the PR description name the requirement ID(s) it satisfies?&lt;/li&gt;
&lt;li&gt;Does every changed file appear in the design plan's affected-files list, or is the extra scope explained?&lt;/li&gt;
&lt;li&gt;Does at least one new or existing test reference each requirement ID touched by this PR?&lt;/li&gt;
&lt;li&gt;If the spec changed, did the code and tests change in the same PR, or is there a tracked follow-up?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Automating Traceability In CI
&lt;/h2&gt;

&lt;p&gt;Manual review catches drift only as often as reviewers remember to look for it, which is why the checks above belong in CI rather than in a wiki page nobody re-reads. The same &lt;a href="https://www.glukhov.org/developer-tools/ci-cd/github-actions-cheatsheet/" rel="noopener noreferrer"&gt;GitHub Actions cheatsheet&lt;/a&gt; patterns you already use for build and test jobs apply directly here — traceability checks are just another job in the same pipeline.&lt;/p&gt;

&lt;p&gt;Practical automation ideas, roughly in order of effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI checks for spec files&lt;/strong&gt; — fail the build if a spec file was edited without a corresponding code or test change in the same PR, or vice versa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require requirement IDs in PR titles or descriptions&lt;/strong&gt; — a lightweight regex check (&lt;code&gt;REQ-\d+&lt;/code&gt;) blocks merges that don't name what they implement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-generated trace summaries&lt;/strong&gt; — have an agent produce a short summary of which requirements a PR touches, for a human to confirm rather than write from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test coverage by acceptance criterion&lt;/strong&gt;, not just by line — line coverage tells you code ran; requirement coverage tells you a claim was checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale spec warnings&lt;/strong&gt; — flag specs that have not been touched in N commits touching their linked files, since long-silent specs are the ones most likely to have quietly rotted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extensions built on top of GitHub's Spec Kit already implement several of these mechanically: one scans literal &lt;code&gt;REQ-XXX&lt;/code&gt; tokens across spec and test files to build a matrix and flag orphaned tests, and a stricter V-Model-oriented pack goes further, generating a paired test specification for every development specification and producing multiple traceability matrices for teams working under regulatory frameworks such as IEC 62304 or ISO 26262. You do not need that level of ceremony for most projects, but the underlying idea — a deterministic, script-generated matrix rather than a hand-maintained spreadsheet — scales down just as well as it scales up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AI Agents For Traceability, Not As An Oracle
&lt;/h2&gt;

&lt;p&gt;AI agents are well suited to the mechanical parts of traceability and poorly suited to being the final judge of whether a requirement was actually satisfied. Three tasks fit an agent's strengths directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compare spec and diff&lt;/strong&gt; — ask the agent to list every requirement mentioned in the spec files touched by a PR, and every one it did not find corresponding code for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find uncovered requirements&lt;/strong&gt; — ask the agent to scan the test suite for requirement tokens and report which requirements in the spec have none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect code not described by spec&lt;/strong&gt; — ask the agent to flag changed files or functions that touch requirement-bearing modules but do not correspond to any requirement ID in the diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure mode to guard against is trusting the agent's summary as ground truth instead of as a reviewer's starting point. An agent can misread a comment, miss a requirement token split across two files, or confidently declare coverage for a test that only exercises the code path superficially. Treat every agent-generated trace report the way you would treat a junior reviewer's pass: useful, fast, and still subject to a second look before it gates a merge. This is the same caution that applies to &lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;decision records for AI-driven development&lt;/a&gt; — the record only stays trustworthy if something other than the agent that wrote it eventually checks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal Traceability Template You Can Copy
&lt;/h2&gt;

&lt;p&gt;You do not need a heavyweight framework to start. A five-file template, checked into the repository next to the code it describes, covers the essentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
  requirements.md     # REQ-IDs with EARS-style acceptance criteria
  design.md           # ADR-IDs, affected files, architecture decisions
  tasks.md            # TASK-IDs mapped to one or more REQ-IDs
  tests.md            # which test files/functions reference which REQ-IDs
  traceability.md     # generated table: REQ -&amp;gt; ADR -&amp;gt; TASK -&amp;gt; files -&amp;gt; tests -&amp;gt; PR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;requirements.md&lt;/code&gt;, &lt;code&gt;design.md&lt;/code&gt;, and &lt;code&gt;tasks.md&lt;/code&gt; are written or edited by humans and agents together, the same way the &lt;a href="https://www.glukhov.org/app-architecture/documentation/spec-driven-development-workflow/" rel="noopener noreferrer"&gt;spec-driven development workflow&lt;/a&gt; already describes. &lt;code&gt;tests.md&lt;/code&gt; and &lt;code&gt;traceability.md&lt;/code&gt; should be generated, not hand-maintained, even if the generator is a short script that just greps for &lt;code&gt;REQ-\d+&lt;/code&gt; across the test directory and the spec files — hand-maintained trace tables are themselves a form of drift risk, because nobody updates a spreadsheet under deadline pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Spec-driven development is not finished the moment code comes out of an agent; it is only useful once code, tests, and specs keep each other honest over time, through PRs, refactors, and requirement changes that arrive months apart. A traceability model built from six plain identifiers, enforced by a handful of CI checks, and reviewed with a short PR checklist gets you most of the benefit without the overhead of a full compliance framework. Start with the minimal template, wire the cheapest CI check first — requirement IDs in PR descriptions — and add the trace table and stale-spec warnings once that habit sticks.&lt;/p&gt;

&lt;p&gt;Traceability is one piece of a larger testing and documentation discipline covered across the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture in Production&lt;/a&gt; cluster, and it sits alongside the tooling questions explored in the &lt;a href="https://www.glukhov.org/ai-devtools/" rel="noopener noreferrer"&gt;AI developer tools&lt;/a&gt; cluster for teams choosing which agent workflows to standardize on.&lt;/p&gt;

</description>
      <category>aicoding</category>
      <category>architecture</category>
      <category>documentation</category>
      <category>llm</category>
    </item>
    <item>
      <title>Dead Letter Queues: Handling Poison Messages in Distributed Systems</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:26:50 +0000</pubDate>
      <link>https://dev.to/rosgluk/dead-letter-queues-handling-poison-messages-in-distributed-systems-7p3</link>
      <guid>https://dev.to/rosgluk/dead-letter-queues-handling-poison-messages-in-distributed-systems-7p3</guid>
      <description>&lt;p&gt;A dead-letter queue is the safety net that catches messages your consumers cannot process, so one broken payload does not block or silently drop everything behind it in the queue.&lt;/p&gt;

&lt;p&gt;Every message-driven system eventually receives a message it cannot handle: a malformed payload, a schema that changed underneath the consumer, or a downstream call that fails no matter how many times you retry it. Without a dead-letter queue, that message either blocks the head of the queue forever or gets silently discarded, and both outcomes are worse than knowing about the failure.&lt;/p&gt;

&lt;p&gt;A DLQ turns an invisible failure into a visible, inspectable one. It gives you a place to quarantine the message, alert on it, and decide — deliberately, not by accident — whether to fix and replay it or discard it for good.&lt;/p&gt;

&lt;p&gt;The mechanics differ across brokers, but the underlying pattern is the same everywhere: a delivery-attempt counter, a threshold, and a destination for messages that cross it. This guide covers what a DLQ actually does, how to tell a poison message from a transient failure, when to retry versus discard, and how to replay safely once you have fixed the root cause. For the broader integration-patterns context this pattern sits inside, see the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Dead Letter Queue
&lt;/h2&gt;

&lt;p&gt;A dead-letter queue is a separate, ordinary queue that a broker or consumer routes a message to after that message fails processing too many times. It is not a special construct — RabbitMQ's dead-letter queue is a regular queue bound to a regular exchange, and an SQS DLQ is a regular standard or FIFO queue. What makes a queue a "DLQ" is purely that something else points failed messages at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    P[Producer] --&amp;gt; Q[Main Queue]
    Q --&amp;gt; C[Consumer]
    C -- ack: success --&amp;gt; Done[Message deleted]
    C -- fail / nack / timeout --&amp;gt; Q
    Q -- retry budget exhausted --&amp;gt; DLQ[Dead Letter Queue]
    DLQ --&amp;gt; I[Inspect / alert]
    I -- fix root cause --&amp;gt; R[Replay to main queue]
    I -- unrecoverable --&amp;gt; D[Archive / discard]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each broker implements the redirect differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SQS&lt;/strong&gt; uses a redrive policy with a &lt;code&gt;maxReceiveCount&lt;/code&gt;. Once a message has been received that many times without being deleted, SQS moves it to the configured &lt;code&gt;deadLetterTargetArn&lt;/code&gt;. AWS explicitly recommends keeping the DLQ's message retention period longer than the source queue's, because the original enqueue timestamp — not the move time — still governs expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RabbitMQ&lt;/strong&gt; dead-letters a message when it is rejected with &lt;code&gt;requeue=false&lt;/code&gt;, its per-message TTL expires, the queue hits a length limit, or a quorum queue exceeds its &lt;code&gt;delivery-limit&lt;/code&gt;. You configure this with the &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; (and optionally &lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;) queue arguments, and RabbitMQ attaches &lt;code&gt;x-death&lt;/code&gt; headers recording the reason, the origin queue, and how many times it happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Kafka&lt;/strong&gt; has no broker-native DLQ. Kafka only tracks offsets; it has no concept of a "failed" message. The dead-letter topic pattern is something you build in the consumer, in a Kafka Streams topology, or in a Kafka Connect connector — commonly paired with a retry-topic tier before the terminal DLT, as Spring Kafka's &lt;code&gt;@RetryableTopic&lt;/code&gt; and &lt;code&gt;DeadLetterPublishingRecoverer&lt;/code&gt; do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Service Bus&lt;/strong&gt; dead-letters automatically once a message's delivery count exceeds &lt;code&gt;MaxDeliveryCount&lt;/code&gt; (default 10), and also for a handful of system reasons such as &lt;code&gt;TTLExpiredException&lt;/code&gt;, &lt;code&gt;HeaderSizeExceeded&lt;/code&gt;, and &lt;code&gt;MaxTransferHopCountExceeded&lt;/code&gt;, each recorded in the message's &lt;code&gt;DeadLetterReason&lt;/code&gt; property.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a broader view of how brokers and streaming platforms fit together operationally rather than as a reliability pattern, &lt;a href="https://www.glukhov.org/data-infrastructure/stream-processing/apache-kafka/" rel="noopener noreferrer"&gt;Apache Kafka Quickstart&lt;/a&gt; and &lt;a href="https://www.glukhov.org/data-infrastructure/messaging/rabbitmq-on-eks-vs-sqs/" rel="noopener noreferrer"&gt;RabbitMQ on AWS EKS vs SQS&lt;/a&gt; cover the infrastructure side of running these brokers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poison Messages
&lt;/h2&gt;

&lt;p&gt;A poison message is one that will never succeed no matter how many times a consumer retries it — a malformed JSON payload, a schema field that a producer renamed, a business rule violation, or a bug that throws on a specific input every single time. That is different from a transient failure, where the message is fine but the environment briefly is not: a downstream timeout, a database connection blip, a rate limit response.&lt;/p&gt;

&lt;p&gt;Treating both failure types the same way is the most common DLQ mistake. If you dead-letter on the first failure, you punish transient errors that would have succeeded on retry. If you retry poison messages dozens of times before giving up, you waste compute, delay unrelated messages behind them (on ordered queues and partitions), and flood your logs with the same stack trace.&lt;/p&gt;

&lt;p&gt;A few detection signals help separate the two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exception type.&lt;/strong&gt; Deserialization errors, validation errors, and &lt;code&gt;ClassCastException&lt;/code&gt;-style failures are almost always permanent. Spring Kafka's &lt;code&gt;DefaultErrorHandler&lt;/code&gt; explicitly treats certain exceptions as fatal and skips retries for them rather than exhausting the retry budget first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat count with no variance.&lt;/strong&gt; RabbitMQ's &lt;code&gt;x-death&lt;/code&gt; header array lets you see exactly how many times a message has been dead-lettered and why; a message with a growing count and an identical &lt;code&gt;x-first-death-reason&lt;/code&gt; on every cycle is poison, not unlucky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent failure across replicas.&lt;/strong&gt; If every consumer instance fails on the same message while succeeding on everything around it, the message itself is the problem, not the infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For distinguishing retryable from non-retryable failures at the code level — the same classification a DLQ policy depends on — see &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture: Boundaries and Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry vs Discard
&lt;/h2&gt;

&lt;p&gt;The core policy decision behind every DLQ is the retry threshold: how many delivery attempts a message gets before it is quarantined. Get this too low and you dead-letter messages that would have succeeded after a brief downstream hiccup. Get it too high and a poison message sits in the main queue for a long time, consuming worker capacity and — on ordered systems — blocking everything queued behind it.&lt;/p&gt;

&lt;p&gt;Current guidance across the major brokers converges on similar numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Broker&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Typical threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SQS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;maxReceiveCount&lt;/code&gt; in redrive policy&lt;/td&gt;
&lt;td&gt;3–5 for mixed transient/permanent workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ (quorum queues)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;delivery-limit&lt;/code&gt; policy argument&lt;/td&gt;
&lt;td&gt;3–5, tuned per queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Service Bus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MaxDeliveryCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default 10, often reduced for latency-sensitive queues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka (via retry topics)&lt;/td&gt;
&lt;td&gt;Retry-count header + retry-topic tier&lt;/td&gt;
&lt;td&gt;3–4 retry-topic hops before the terminal DLT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A practical middle ground many teams land on is: start conservative (2–3 attempts), watch the actual failure mix in production, and raise the threshold only for queues where you can show most failures resolve within a few retries. Pair the retry count with &lt;strong&gt;exponential backoff and jitter&lt;/strong&gt; between attempts so a downstream outage does not turn into a retry storm — the same discipline covered in backoff and circuit-breaker design. A &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/circuit-breaker-pattern-in-go/" rel="noopener noreferrer"&gt;circuit breaker at the integration boundary&lt;/a&gt; complements this: it stops sending requests to an unhealthy dependency instead of letting every message in the queue individually discover the outage and dead-letter one by one.&lt;/p&gt;

&lt;p&gt;Once a message is in the DLQ, "discard" should still be a deliberate action, not neglect. Set a retention period on the DLQ itself — long enough to investigate (AWS recommends the DLQ retention exceed the source queue's; a week is a common floor for RabbitMQ DLQs) — and alert on DLQ depth and age so failures get triaged instead of silently expiring. A message that ages out of the DLQ unexamined is a message you decided to lose without deciding to lose it.&lt;/p&gt;

&lt;p&gt;Idempotency matters just as much here as it does anywhere else duplicates can occur: a message that gets redriven from a DLQ back to the main queue is, functionally, a duplicate delivery. If your consumer is not safe to run twice on the same message, redriving from a DLQ can create the exact duplicate-side-effect bug you were trying to avoid. See &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems That Actually Works&lt;/a&gt; for the consumer-side patterns that make redrive safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay Strategies
&lt;/h2&gt;

&lt;p&gt;Getting a message out of the DLQ correctly is its own discipline, separate from getting it in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix the root cause first.&lt;/strong&gt; Deploying the consumer fix before replaying is the difference between a clean recovery and re-poisoning the queue with the same failure a second time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redrive deliberately, not automatically.&lt;/strong&gt; SQS supports a redrive-to-source feature that moves messages back to their original queue (or another destination) on demand; RabbitMQ and Kafka require you to build the equivalent consumer or tooling yourself. Either way, treat replay as an operator-triggered action with a record of what was replayed and when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve ordering where it matters.&lt;/strong&gt; For Kafka, the dead-letter topic should have at least as many partitions as the source topic and should retain the original message key, so that replayed messages land back on the correct partition and preserve per-key ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap replay attempts.&lt;/strong&gt; A message that fails again after a fix-and-replay cycle is not transient — route it to a permanent archive (a database table, an object-storage bucket) instead of looping it through the DLQ indefinitely. RabbitMQ's own docs warn that a dead-lettered message can be routed between queues only a limited number of times (16) before further TTL-based dead-lettering is disabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let a DLQ dead-letter into itself.&lt;/strong&gt; If your DLQ has its own &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; (RabbitMQ) or its own redrive policy (SQS) pointed back at the same chain, a replay failure can create an infinite loop. Keep the DLQ's own dead-letter configuration empty, or point it at a strictly terminal archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on volume, not just presence.&lt;/strong&gt; A single message in a DLQ is a data point; a sudden spike is an incident. Wire DLQ depth and message age into the same alerting pipeline you use for everything else — see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt; for routing and noise-reduction practices that apply directly to DLQ alerts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your workflow involves multi-step, long-running processes rather than single messages, the same dead-letter thinking applies at the workflow layer — a &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/saga-pattern-distributed-transactions/" rel="noopener noreferrer"&gt;saga's compensation logic&lt;/a&gt; needs the same "quarantine, inspect, decide" discipline when a step fails permanently instead of transiently. And when the events themselves originate from a database write, the &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/transactional-outbox-pattern-go/" rel="noopener noreferrer"&gt;transactional outbox pattern&lt;/a&gt; already builds dead-letter handling into the relay worker, so the pattern shows up one layer earlier than the broker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DLQs Fit in the Bigger Picture
&lt;/h2&gt;

&lt;p&gt;A dead-letter queue does not make failures go away — it makes them survivable and reviewable instead of silent. It works best alongside retries with backoff for the transient case, idempotent consumers so redrive is safe, and a circuit breaker so a struggling dependency does not flood the main queue (and, eventually, the DLQ) with the same failure thousands of times over. Treat the DLQ threshold, retention, and alerting as first-class configuration decisions, not defaults you leave untouched, and dead letters become a diagnostic tool instead of a place where data quietly disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html" rel="noopener noreferrer"&gt;Amazon SQS Developer Guide, Dead-letter queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/docs/dlx" rel="noopener noreferrer"&gt;RabbitMQ Documentation, Dead Letter Exchanges&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.enterprise.spring.io/spring-kafka/reference/retrytopic/dlt-strategies.html" rel="noopener noreferrer"&gt;Spring Kafka Documentation, DLT Strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues" rel="noopener noreferrer"&gt;Microsoft Learn, Service Bus dead-letter queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://factorhouse.io/articles/dead-letter-queues-kafka" rel="noopener noreferrer"&gt;Factor House, Dead letter queues in Kafka: patterns and pitfalls&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dev</category>
      <category>microservices</category>
    </item>
    <item>
      <title>LLM Wiki Maintenance: Drift, Contradictions and Review</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:29:50 +0000</pubDate>
      <link>https://dev.to/rosgluk/llm-wiki-maintenance-drift-contradictions-and-review-4bp1</link>
      <guid>https://dev.to/rosgluk/llm-wiki-maintenance-drift-contradictions-and-review-4bp1</guid>
      <description>&lt;p&gt;An LLM Wiki fails when old facts remain plausible, contradictions become polished, and generated summaries drift from their sources.&lt;/p&gt;

&lt;p&gt;Maintenance is the real product of any compiled knowledge system. Creating wiki pages is straightforward compared with keeping them trustworthy across months of ingest, edits, rewrites, and new sources.&lt;/p&gt;

&lt;p&gt;This article covers the operational side of LLM Wiki systems: drift detection, contradiction checks, citation discipline, linting, Git review, and maintenance workflows. It assumes you already understand the basic pattern described in &lt;a href="https://www.glukhov.org/knowledge-management/knowledge-systems-architectures/compiled-knowledge/what-is-llm-wiki/" rel="noopener noreferrer"&gt;LLM Wiki - Compiled Knowledge That RAG Cannot Replace&lt;/a&gt;: raw sources are compiled into durable Markdown pages that humans and agents can query later. The calm but opinionated view is that an LLM Wiki without maintenance is just a nicer-looking knowledge graveyard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes LLM Wiki Maintenance Different
&lt;/h2&gt;

&lt;p&gt;Traditional wikis rot because people stop updating them. RAG systems drift because the corpus changes, chunks get stale, metadata is weak, and retrieval keeps finding plausible but outdated fragments. The deeper reason is that &lt;a href="https://www.glukhov.org/knowledge-management/foundations/retrieval-vs-representation/" rel="noopener noreferrer"&gt;retrieval and representation solve different problems&lt;/a&gt;: retrieval can be re-run against fresh data, but a representation you already compiled has to be actively kept honest.&lt;/p&gt;

&lt;p&gt;An LLM Wiki has a different failure mode. It can look clean even when it is wrong.&lt;/p&gt;

&lt;p&gt;The pages may be well formatted. The links may work. The summaries may sound balanced. But underneath that neat surface, the system may have dropped critical facts, merged incompatible concepts, cited summaries instead of sources, or preserved an old decision as if it still applied.&lt;/p&gt;

&lt;p&gt;That is why LLM Wiki maintenance must check both structure and meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintenance Goal
&lt;/h2&gt;

&lt;p&gt;The goal is not to make every page perfect. That is too expensive, and it usually leads to abandoned systems.&lt;/p&gt;

&lt;p&gt;The goal is to keep the wiki useful, inspectable, and recoverable.&lt;/p&gt;

&lt;p&gt;A maintained LLM Wiki should make it easy to answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What sources support this claim?&lt;/li&gt;
&lt;li&gt;When was this page last reviewed?&lt;/li&gt;
&lt;li&gt;Has this concept changed?&lt;/li&gt;
&lt;li&gt;Are there conflicting pages?&lt;/li&gt;
&lt;li&gt;Is this summary still current?&lt;/li&gt;
&lt;li&gt;Did the agent rewrite more than it should?&lt;/li&gt;
&lt;li&gt;Can we roll back a bad update?&lt;/li&gt;
&lt;li&gt;Can a human understand why the page says what it says?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, the problem is not only content quality. It is system design.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Maintenance Loop
&lt;/h2&gt;

&lt;p&gt;A useful LLM Wiki needs a repeatable loop. The loop should be simple enough to run often and strict enough to catch drift before it becomes invisible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Add or update source] --&amp;gt; B[Compile into wiki pages]
    B --&amp;gt; C[Update links and indexes]
    C --&amp;gt; D[Run structural lint checks]
    D --&amp;gt; E[Run semantic review checks]
    E --&amp;gt; F[Human review of risky changes]
    F --&amp;gt; G[Commit approved changes]
    G --&amp;gt; H[Schedule stale-page review]
    H --&amp;gt; A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop is not glamorous. That is the point.&lt;/p&gt;

&lt;p&gt;A knowledge system becomes durable through boring maintenance: source preservation, explicit review, predictable structure, and small safe updates. The same instinct drives &lt;a href="https://www.glukhov.org/knowledge-management/methods/evergreen-notes/" rel="noopener noreferrer"&gt;evergreen notes&lt;/a&gt;: a note or a wiki page only compounds in value if someone keeps refining it instead of leaving it to rot after the first draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Drift in an LLM Wiki
&lt;/h2&gt;

&lt;p&gt;Knowledge drift is not one thing. Different drift types require different checks. A good maintenance system should name them clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Source Drift
&lt;/h3&gt;

&lt;p&gt;Source drift happens when the underlying source material changes.&lt;/p&gt;

&lt;p&gt;For example, a tool releases a new version, a policy is updated, an API changes, or a vendor deprecates an old feature. The old wiki page may still be accurate for the previous version, but wrong for current use.&lt;/p&gt;

&lt;p&gt;Source drift is dangerous because the old claim may still be true in historical context. The problem is not that the claim is fake. The problem is that it no longer answers the current question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Record source dates&lt;/li&gt;
&lt;li&gt;Record last reviewed dates&lt;/li&gt;
&lt;li&gt;Mark version-specific pages clearly&lt;/li&gt;
&lt;li&gt;Link old pages to superseding pages&lt;/li&gt;
&lt;li&gt;Avoid mixing old and new versions without labels&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Concept Drift
&lt;/h3&gt;

&lt;p&gt;Concept drift happens when the meaning of a term changes over time.&lt;/p&gt;

&lt;p&gt;This is common in AI and software architecture. Terms like "agent", "memory", "RAG", "workflow", "tool use", "structured output", and "context engineering" can shift meaning quickly.&lt;/p&gt;

&lt;p&gt;A wiki can accidentally preserve several meanings of the same term without explaining the difference. That creates confusing pages that sound coherent but combine incompatible ideas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain glossary pages&lt;/li&gt;
&lt;li&gt;Add "meaning in this wiki" sections&lt;/li&gt;
&lt;li&gt;Separate overloaded concepts into distinct pages&lt;/li&gt;
&lt;li&gt;Link related meanings explicitly&lt;/li&gt;
&lt;li&gt;Avoid letting the model silently merge terms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Terminology Drift
&lt;/h3&gt;

&lt;p&gt;Terminology drift is smaller than concept drift but still harmful.&lt;/p&gt;

&lt;p&gt;It happens when the wiki uses multiple names for the same thing: "LLM Wiki", "compiled knowledge base", "AI-maintained wiki", "agent-maintained wiki", and "Markdown knowledge base".&lt;/p&gt;

&lt;p&gt;Some variation is fine. Too much variation breaks search, linking, and review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define canonical page names&lt;/li&gt;
&lt;li&gt;Keep aliases in front matter or page metadata&lt;/li&gt;
&lt;li&gt;Redirect duplicate pages&lt;/li&gt;
&lt;li&gt;Lint for near-duplicate titles&lt;/li&gt;
&lt;li&gt;Use consistent anchor text in internal links&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Decision Drift
&lt;/h3&gt;

&lt;p&gt;Decision drift happens when a past decision remains documented but no longer reflects current practice.&lt;/p&gt;

&lt;p&gt;For example, a page might say that the project uses vector RAG for all document search, while newer pages describe an LLM Wiki workflow. Both statements may be historically true, but the wiki must show which one is current.&lt;/p&gt;

&lt;p&gt;This matters for architecture notes, engineering processes, content strategy, and internal tooling. The same discipline applies to &lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;decision records in AI-driven development&lt;/a&gt;: a decision page is only trustworthy if superseded choices are marked as superseded rather than silently overwritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark decisions as proposed, accepted, superseded, or rejected&lt;/li&gt;
&lt;li&gt;Keep decision dates&lt;/li&gt;
&lt;li&gt;Link superseded decisions to replacements&lt;/li&gt;
&lt;li&gt;Preserve historical context&lt;/li&gt;
&lt;li&gt;Avoid deleting old decisions without trace&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Citation Drift
&lt;/h3&gt;

&lt;p&gt;Citation drift happens when a page cites a source, but the claim no longer matches what the source says.&lt;/p&gt;

&lt;p&gt;This can happen after a rewrite. The citation remains in place, but the sentence around it changes. The page still looks sourced, yet the citation no longer supports the claim.&lt;/p&gt;

&lt;p&gt;This is one of the most serious LLM Wiki failure modes because it creates false confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check claim-level citation support&lt;/li&gt;
&lt;li&gt;Avoid citing only summaries&lt;/li&gt;
&lt;li&gt;Keep raw sources&lt;/li&gt;
&lt;li&gt;Require citations for important claims&lt;/li&gt;
&lt;li&gt;Flag paragraphs with sources but no direct support&lt;/li&gt;
&lt;li&gt;Review citation changes in Git diffs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Structure Drift
&lt;/h3&gt;

&lt;p&gt;Structure drift happens when the wiki slowly loses navigability.&lt;/p&gt;

&lt;p&gt;New pages are created instead of updating old ones. Index pages fall behind. Duplicate pages appear. Orphan pages accumulate. Related pages stop linking to each other.&lt;/p&gt;

&lt;p&gt;The wiki still contains useful knowledge, but finding and trusting it becomes harder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lint for orphan pages&lt;/li&gt;
&lt;li&gt;Maintain index pages&lt;/li&gt;
&lt;li&gt;Detect duplicate topics&lt;/li&gt;
&lt;li&gt;Require backlinks for canonical pages&lt;/li&gt;
&lt;li&gt;Archive rather than scatter&lt;/li&gt;
&lt;li&gt;Keep folder roles clear&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Core Maintenance Files
&lt;/h2&gt;

&lt;p&gt;An LLM Wiki should not rely on memory, vibes, or repeated prompting. It needs operating files that tell humans and agents how to maintain it.&lt;/p&gt;

&lt;p&gt;A practical structure can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;llm-wiki/
  raw/
    sources/
    transcripts/
    documents/
  wiki/
    index.md
    concepts/
    entities/
    projects/
    decisions/
  maintenance/
    review-log.md
    lint-report.md
    stale-pages.md
    contradiction-report.md
  rules/
    AGENTS.md
    schema.md
    style-guide.md
    citation-policy.md
    source-policy.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact names do not matter. The roles do.&lt;/p&gt;

&lt;p&gt;The wiki needs source storage, compiled pages, review artifacts, and rules that survive across sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  AGENTS.md
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; should explain how the agent is allowed to work.&lt;/p&gt;

&lt;p&gt;It should answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should the agent update existing pages before creating new ones?&lt;/li&gt;
&lt;li&gt;When should it ask for review?&lt;/li&gt;
&lt;li&gt;How should it cite sources?&lt;/li&gt;
&lt;li&gt;Can it reorganize folders?&lt;/li&gt;
&lt;li&gt;Can it rewrite old pages?&lt;/li&gt;
&lt;li&gt;What should it do with contradictions?&lt;/li&gt;
&lt;li&gt;What should it never delete?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prefer small, source-backed updates over broad rewrites.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one sentence prevents many bad maintenance habits. It also draws a useful boundary relative to &lt;a href="https://www.glukhov.org/ai-systems/memory/memory-systems-in-ai-assistants/" rel="noopener noreferrer"&gt;agent memory&lt;/a&gt;: memory shapes how an agent behaves in the moment, while the wiki's rules file shapes what the agent is allowed to change in the shared knowledge base.&lt;/p&gt;

&lt;h3&gt;
  
  
  schema.md
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;schema.md&lt;/code&gt; should define the page structure.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Page Title

## Summary
Short current summary.

## Key Claims
- Claim with source reference.
- Claim with source reference.

## Current Status
Current, historical, superseded, draft, or uncertain.

## Details
Main compiled explanation.

## Related Pages
Internal links.

## Sources
Raw source references.

## Review Notes
Last reviewed date and reviewer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A schema does not need to be rigid. But without a schema, the wiki becomes a pile of inconsistent essays. The schema gives the agent something to preserve.&lt;/p&gt;

&lt;h3&gt;
  
  
  citation-policy.md
&lt;/h3&gt;

&lt;p&gt;The citation policy should say what requires a source. At minimum, require citations for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical claims&lt;/li&gt;
&lt;li&gt;Comparisons&lt;/li&gt;
&lt;li&gt;Product behavior&lt;/li&gt;
&lt;li&gt;Version-specific statements&lt;/li&gt;
&lt;li&gt;Benchmark results&lt;/li&gt;
&lt;li&gt;Pricing&lt;/li&gt;
&lt;li&gt;Legal or compliance claims&lt;/li&gt;
&lt;li&gt;Current status claims&lt;/li&gt;
&lt;li&gt;Claims copied or derived from a source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good citation policy also says what not to do. Do not cite a compiled page as if it were the original source. Do not attach citations to paragraphs they do not support. Do not keep a citation after rewriting a claim unless the source still supports it.&lt;/p&gt;

&lt;p&gt;Citation discipline is the difference between a useful LLM Wiki and decorative confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  source-policy.md
&lt;/h3&gt;

&lt;p&gt;The source policy should define how raw sources are stored.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep original files when possible&lt;/li&gt;
&lt;li&gt;Preserve URLs and access dates&lt;/li&gt;
&lt;li&gt;Store copied notes separately from generated summaries&lt;/li&gt;
&lt;li&gt;Record source type&lt;/li&gt;
&lt;li&gt;Mark low-confidence sources&lt;/li&gt;
&lt;li&gt;Avoid overwriting raw material&lt;/li&gt;
&lt;li&gt;Keep superseded sources when they explain historical decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This policy protects the wiki from a common mistake: summarizing a source, discarding the original, and then treating the summary as evidence.&lt;/p&gt;

&lt;p&gt;That is not knowledge management. That is lossy compression.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an LLM Wiki Linter Should Check
&lt;/h2&gt;

&lt;p&gt;A linter should check structure first. Structural problems are easier to automate and often reveal deeper semantic problems.&lt;/p&gt;

&lt;p&gt;Useful structural checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broken internal links&lt;/li&gt;
&lt;li&gt;Orphan pages&lt;/li&gt;
&lt;li&gt;Duplicate titles&lt;/li&gt;
&lt;li&gt;Missing source sections&lt;/li&gt;
&lt;li&gt;Missing review dates&lt;/li&gt;
&lt;li&gt;Pages without backlinks&lt;/li&gt;
&lt;li&gt;Empty or placeholder sections&lt;/li&gt;
&lt;li&gt;Very long pages without section structure&lt;/li&gt;
&lt;li&gt;Pages with no incoming links from an index&lt;/li&gt;
&lt;li&gt;Inconsistent naming conventions&lt;/li&gt;
&lt;li&gt;Invalid front matter&lt;/li&gt;
&lt;li&gt;Stale "current" pages older than a review threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks do not prove the knowledge is true. They prove the wiki is still maintainable. That is a necessary starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic Checks
&lt;/h2&gt;

&lt;p&gt;Semantic checks are harder but more valuable.&lt;/p&gt;

&lt;p&gt;Useful semantic checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claims without direct source support&lt;/li&gt;
&lt;li&gt;Two pages making incompatible claims&lt;/li&gt;
&lt;li&gt;Old decisions presented as current&lt;/li&gt;
&lt;li&gt;Duplicate concepts with different names&lt;/li&gt;
&lt;li&gt;One concept page mixing several meanings&lt;/li&gt;
&lt;li&gt;Summaries that omit known constraints&lt;/li&gt;
&lt;li&gt;"Best" or "recommended" claims without criteria&lt;/li&gt;
&lt;li&gt;Version-specific claims without version labels&lt;/li&gt;
&lt;li&gt;Pages that contradict newer sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks should not automatically rewrite the wiki. They should usually produce a report for review.&lt;/p&gt;

&lt;p&gt;The safer pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Detect automatically.
Explain clearly.
Update deliberately.
Review risky changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Contradiction Detection
&lt;/h2&gt;

&lt;p&gt;Contradiction detection is not just asking an LLM whether two pages contradict each other.&lt;/p&gt;

&lt;p&gt;That can help, but it is too vague. A better approach is to compare claims.&lt;/p&gt;

&lt;p&gt;A contradiction workflow can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Extract claims from page] --&amp;gt; B[Find related pages and sources]
    B --&amp;gt; C[Extract claims from related material]
    C --&amp;gt; D[Group claims by subject]
    D --&amp;gt; E[Compare status, date, version, and scope]
    E --&amp;gt; F{Conflict found?}
    F --&amp;gt;|No| G[No action]
    F --&amp;gt;|Yes| H[Classify conflict]
    H --&amp;gt; I[Create contradiction report]
    I --&amp;gt; J[Human or agent-assisted resolution]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works better because many apparent contradictions are not real contradictions.&lt;/p&gt;

&lt;p&gt;One page may describe version 1.0 and another version 2.0. One page may describe personal use and another enterprise use. One page may describe the design goal and another the implementation reality.&lt;/p&gt;

&lt;p&gt;The contradiction report should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conflicting claims&lt;/li&gt;
&lt;li&gt;Pages involved&lt;/li&gt;
&lt;li&gt;Source references&lt;/li&gt;
&lt;li&gt;Date or version context&lt;/li&gt;
&lt;li&gt;Likely explanation&lt;/li&gt;
&lt;li&gt;Suggested resolution&lt;/li&gt;
&lt;li&gt;Whether human review is required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not let the agent silently resolve contradictions by blending both claims into a vague compromise. That creates smooth nonsense.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Resolve Contradictions
&lt;/h2&gt;

&lt;p&gt;There are several valid ways to resolve a contradiction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If one claim is outdated, mark it as superseded and link to the newer page.&lt;/li&gt;
&lt;li&gt;If both claims are true in different contexts, split the context clearly.&lt;/li&gt;
&lt;li&gt;If the sources disagree, preserve the disagreement and explain it.&lt;/li&gt;
&lt;li&gt;If the wiki page invented or overgeneralized a claim, remove or narrow the claim.&lt;/li&gt;
&lt;li&gt;If the contradiction reflects a real unresolved decision, create or update a decision page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to remove all tension. Some tension is useful. A good wiki shows where knowledge is settled and where it is still uncertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stale Page Review
&lt;/h2&gt;

&lt;p&gt;Every compiled page should have a review status.&lt;/p&gt;

&lt;p&gt;A simple metadata block can be enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
&lt;span class="na"&gt;last_reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-07-10&lt;/span&gt;
&lt;span class="na"&gt;review_after&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-10&lt;/span&gt;
&lt;span class="na"&gt;source_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For fast-moving topics, use short review windows. For stable concepts, use longer windows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page type&lt;/th&gt;
&lt;th&gt;Review interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool version pages&lt;/td&gt;
&lt;td&gt;30 to 90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing or availability pages&lt;/td&gt;
&lt;td&gt;7 to 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture principles&lt;/td&gt;
&lt;td&gt;6 to 18 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical decision records&lt;/td&gt;
&lt;td&gt;Only when superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glossary pages&lt;/td&gt;
&lt;td&gt;3 to 12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source summaries&lt;/td&gt;
&lt;td&gt;When source changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparison pages&lt;/td&gt;
&lt;td&gt;30 to 180 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interval is less important than the habit. A page without a review date is a page that will eventually lie quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source-Aware Updates
&lt;/h2&gt;

&lt;p&gt;When a new source arrives, the agent should not simply write a new summary page.&lt;/p&gt;

&lt;p&gt;It should ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this update an existing concept?&lt;/li&gt;
&lt;li&gt;Does it supersede a previous source?&lt;/li&gt;
&lt;li&gt;Does it contradict an existing page?&lt;/li&gt;
&lt;li&gt;Does it add a new entity or term?&lt;/li&gt;
&lt;li&gt;Does it change a recommendation?&lt;/li&gt;
&lt;li&gt;Does it require an index update?&lt;/li&gt;
&lt;li&gt;Does it affect decision pages?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good ingest workflow updates the wiki as a graph, not as a stack of isolated summaries.&lt;/p&gt;

&lt;p&gt;This is why one source may touch many pages. That is normal. The maintenance challenge is making those updates small, reviewable, and traceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review with Git Diffs
&lt;/h2&gt;

&lt;p&gt;Git is one of the best maintenance tools for an LLM Wiki.&lt;/p&gt;

&lt;p&gt;Not because Git is fashionable, but because generated knowledge needs reviewable change history. A Git diff shows what the agent changed, deleted, moved, or rephrased.&lt;/p&gt;

&lt;p&gt;Use Git to review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New pages&lt;/li&gt;
&lt;li&gt;Deleted sections&lt;/li&gt;
&lt;li&gt;Changed claims&lt;/li&gt;
&lt;li&gt;Changed citations&lt;/li&gt;
&lt;li&gt;Renamed pages&lt;/li&gt;
&lt;li&gt;Index updates&lt;/li&gt;
&lt;li&gt;Link changes&lt;/li&gt;
&lt;li&gt;Status changes&lt;/li&gt;
&lt;li&gt;Broad rewrites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important review habit is to inspect deletions. LLMs often remove details while making prose cleaner.&lt;/p&gt;

&lt;p&gt;Clean prose is not always better knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Commit Strategy
&lt;/h2&gt;

&lt;p&gt;Avoid giant commits like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update wiki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use commits that explain the operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ingest: add source notes for Qwen embedding release
maint: mark old vector store comparison as superseded
lint: fix broken internal links in RAG pages
review: update LLM Wiki page citations
refactor: split agent memory concept from long-term memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives future humans and agents a useful history.&lt;/p&gt;

&lt;p&gt;Commit messages are part of the knowledge system. They explain why the wiki changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Levels
&lt;/h2&gt;

&lt;p&gt;Not every page needs the same review level. Use risk-based review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-risk changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broken link fixes&lt;/li&gt;
&lt;li&gt;Formatting cleanup&lt;/li&gt;
&lt;li&gt;Adding backlinks&lt;/li&gt;
&lt;li&gt;Adding index entries&lt;/li&gt;
&lt;li&gt;Correcting typos&lt;/li&gt;
&lt;li&gt;Adding aliases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Medium-risk changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding source summaries&lt;/li&gt;
&lt;li&gt;Creating new concept pages&lt;/li&gt;
&lt;li&gt;Updating recommendations&lt;/li&gt;
&lt;li&gt;Merging duplicate pages&lt;/li&gt;
&lt;li&gt;Changing page status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;High-risk changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleting claims&lt;/li&gt;
&lt;li&gt;Rewriting canonical pages&lt;/li&gt;
&lt;li&gt;Changing decision records&lt;/li&gt;
&lt;li&gt;Updating security, legal, pricing, or benchmark claims&lt;/li&gt;
&lt;li&gt;Resolving contradictions&lt;/li&gt;
&lt;li&gt;Marking content as current or superseded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;High-risk changes should get human review. That may be you, a maintainer, or a domain owner.&lt;/p&gt;

&lt;p&gt;The agent can prepare the change, but it should not always be the final authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Maintenance Dashboard
&lt;/h2&gt;

&lt;p&gt;A simple maintenance dashboard can live in Markdown.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# LLM Wiki Maintenance Dashboard

## Needs Review
- wiki/concepts/agent-memory.md - review overdue
- wiki/projects/search-index.md - conflicting claims found

## Stale Pages
- wiki/tools/ollama.md - last reviewed 120 days ago
- wiki/concepts/rag.md - fast-moving topic, review due

## Broken Links
- wiki/index.md -&amp;gt; wiki/concepts/old-page.md

## Orphan Pages
- wiki/entities/vendor-x.md

## Contradiction Reports
- reports/contradictions/2026-07-10-agent-memory.md

## Recent Ingests
- raw/sources/2026-07-09-new-paper.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a fancy admin panel. It is a working surface.&lt;/p&gt;

&lt;p&gt;For many solo or small-team systems, a Markdown dashboard is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics for a Healthy LLM Wiki
&lt;/h2&gt;

&lt;p&gt;You can track wiki health without overengineering it.&lt;/p&gt;

&lt;p&gt;Useful metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of pages&lt;/li&gt;
&lt;li&gt;Pages without sources&lt;/li&gt;
&lt;li&gt;Pages without review dates&lt;/li&gt;
&lt;li&gt;Pages past review date&lt;/li&gt;
&lt;li&gt;Broken internal links&lt;/li&gt;
&lt;li&gt;Orphan pages&lt;/li&gt;
&lt;li&gt;Duplicate titles or aliases&lt;/li&gt;
&lt;li&gt;Contradiction reports open&lt;/li&gt;
&lt;li&gt;Contradiction reports resolved&lt;/li&gt;
&lt;li&gt;Average page age by cluster&lt;/li&gt;
&lt;li&gt;Raw sources not yet ingested&lt;/li&gt;
&lt;li&gt;Pages changed without human review&lt;/li&gt;
&lt;li&gt;Canonical pages changed in the last 30 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics are not vanity numbers. They tell you whether the system is becoming easier or harder to trust.&lt;/p&gt;

&lt;p&gt;A growing wiki with rising orphan pages and stale reviews is not compounding knowledge. It is compounding maintenance debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance Prompts
&lt;/h2&gt;

&lt;p&gt;Maintenance prompts should be narrow. Broad prompts produce broad rewrites.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural Lint Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review the wiki structure.

Check for:
- broken internal links
- orphan pages
- duplicate or near-duplicate page titles
- pages missing source sections
- pages missing last_reviewed metadata
- index pages that do not link to new pages

Do not rewrite content.
Produce a Markdown report with findings and suggested fixes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Citation Review Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review citations for the selected page.

For each important claim:
- identify the supporting source
- check whether the source directly supports the claim
- flag claims with weak, missing, or mismatched support
- do not rewrite the page yet

Return a table with:
claim, current citation, support level, issue, suggested action.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Contradiction Check Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check this page for contradictions against related pages.

Steps:
1. Extract the main claims from the page.
2. Find related pages through links, backlinks, aliases, and search.
3. Extract potentially conflicting claims.
4. Classify each conflict as:
   - real contradiction
   - version difference
   - scope difference
   - terminology difference
   - unresolved uncertainty
5. Recommend a resolution.

Do not edit files until the contradiction report is reviewed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stale Page Review Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this page for staleness.

Check:
- dates
- version-specific claims
- tool behavior
- recommendations
- links to superseded pages
- sources newer than the page review date
- claims marked current without recent support

Return:
- keep current
- update needed
- mark historical
- mark superseded
- split into versioned pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Safe Update Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update the selected wiki page using the provided source.

Rules:
- preserve existing useful structure
- do not remove sourced claims unless the source is superseded
- add citations for new claims
- mark uncertainty explicitly
- update related pages only when necessary
- add a review note explaining what changed
- keep the diff small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These prompts are not magic. They are guardrails. The real value comes from making the maintenance action explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Maintenance Mistakes
&lt;/h2&gt;

&lt;p&gt;The first mistake is letting the agent rewrite too much.&lt;/p&gt;

&lt;p&gt;Large rewrites feel productive because they make pages smoother. They also make it harder to see what changed and easier to lose sharp details.&lt;/p&gt;

&lt;p&gt;The second mistake is treating generated summaries as sources.&lt;/p&gt;

&lt;p&gt;A summary can be useful, but it is not the evidence. Keep raw sources and cite them when claims matter.&lt;/p&gt;

&lt;p&gt;The third mistake is creating new pages instead of updating old ones.&lt;/p&gt;

&lt;p&gt;This produces duplicate concepts, conflicting recommendations, and search confusion. The agent should search first, update second, and create new pages only when the concept is genuinely new.&lt;/p&gt;

&lt;p&gt;The fourth mistake is deleting historical context.&lt;/p&gt;

&lt;p&gt;Old decisions, failed experiments, and superseded recommendations can be valuable. Mark them clearly instead of erasing them.&lt;/p&gt;

&lt;p&gt;The fifth mistake is forgetting indexes.&lt;/p&gt;

&lt;p&gt;A page that is not linked from the right place is half-lost. Index maintenance is not clerical work. It is navigation architecture.&lt;/p&gt;

&lt;p&gt;The sixth mistake is using the same review policy for every page.&lt;/p&gt;

&lt;p&gt;A stable conceptual page and a fast-moving tool comparison should not have the same review schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Archive, Supersede, or Delete
&lt;/h2&gt;

&lt;p&gt;Most stale pages should not be deleted immediately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&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;Archive&lt;/td&gt;
&lt;td&gt;The page is historical but still useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supersede&lt;/td&gt;
&lt;td&gt;A newer page replaces the old answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;The page is duplicate, empty, wrong, or unrecoverable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A superseded page should link to the replacement.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: superseded
Superseded by: wiki/concepts/agent-memory-architecture.md
Reason: This page used an older definition of agent memory before the project separated session memory, user memory, and compiled knowledge.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves context and reduces confusion.&lt;/p&gt;

&lt;p&gt;Deletion should be rare and reviewable. Knowledge systems need pruning, but invisible pruning is dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical Pages Need Extra Care
&lt;/h2&gt;

&lt;p&gt;Every LLM Wiki eventually develops canonical pages.&lt;/p&gt;

&lt;p&gt;These are pages that define core concepts, architecture choices, workflows, or project vocabulary. They are more important than ordinary notes because many other pages depend on them.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is LLM Wiki&lt;/li&gt;
&lt;li&gt;RAG vs compiled knowledge&lt;/li&gt;
&lt;li&gt;Agent memory&lt;/li&gt;
&lt;li&gt;Source policy&lt;/li&gt;
&lt;li&gt;Review workflow&lt;/li&gt;
&lt;li&gt;Project architecture&lt;/li&gt;
&lt;li&gt;Glossary&lt;/li&gt;
&lt;li&gt;Index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes to canonical pages should require stricter review.&lt;/p&gt;

&lt;p&gt;A small error on an obscure page is local. A small error on a canonical page can distort the whole wiki.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Often Should You Run Maintenance
&lt;/h2&gt;

&lt;p&gt;For an active LLM Wiki, run small maintenance frequently and deep maintenance occasionally.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Maintenance task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every ingest&lt;/td&gt;
&lt;td&gt;Update links, sources, indexes, and review notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;td&gt;Run structural lint checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;Review stale pages and orphan pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;Check contradictions in active clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;Review canonical pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;Archive or supersede old pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before major use&lt;/td&gt;
&lt;td&gt;Run citation checks on pages used for reports, articles, or decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The schedule should match risk. A personal research wiki can be lighter. A wiki used for technical publishing, customer support, or engineering decisions needs more discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using an LLM Wiki for Technical Publishing
&lt;/h2&gt;

&lt;p&gt;For a technical blog or documentation site, the LLM Wiki can become the knowledge layer behind article planning.&lt;/p&gt;

&lt;p&gt;It can track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canonical explanations&lt;/li&gt;
&lt;li&gt;Repeated definitions&lt;/li&gt;
&lt;li&gt;Internal link opportunities&lt;/li&gt;
&lt;li&gt;Stale comparisons&lt;/li&gt;
&lt;li&gt;Article gaps&lt;/li&gt;
&lt;li&gt;Topic clusters&lt;/li&gt;
&lt;li&gt;Source notes&lt;/li&gt;
&lt;li&gt;Decisions about structure&lt;/li&gt;
&lt;li&gt;Claims that need verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is useful, but it raises the maintenance bar.&lt;/p&gt;

&lt;p&gt;If the wiki feeds published articles, then stale wiki pages can become stale public content. If the wiki contains contradictions, those contradictions may leak into articles. If citations are weak, generated drafts will inherit that weakness. The scoped summaries, schema-based extraction, and human review loops described in &lt;a href="https://www.glukhov.org/knowledge-management/ai-augmented-knowledge/ai-for-knowledge-management-workflows/" rel="noopener noreferrer"&gt;AI for Knowledge Management: Real Workflows That Hold Up&lt;/a&gt; are a good template for keeping that editorial pipeline honest.&lt;/p&gt;

&lt;p&gt;For publishing workflows, treat the LLM Wiki as an editorial system, not just a private notebook. A &lt;a href="https://www.glukhov.org/knowledge-management/methods/digital-gardening/" rel="noopener noreferrer"&gt;digital garden&lt;/a&gt; makes this explicit by showing readers which pages are still growing and which are mature, which is exactly the kind of status signal a well-maintained LLM Wiki should also expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance Is Not Optional
&lt;/h2&gt;

&lt;p&gt;An LLM Wiki is attractive because it promises compounding knowledge.&lt;/p&gt;

&lt;p&gt;But compounding only works if old knowledge remains useful. Otherwise, the system compounds noise, not insight.&lt;/p&gt;

&lt;p&gt;The maintenance burden does not disappear because an agent is involved. It changes shape.&lt;/p&gt;

&lt;p&gt;Humans should not have to do every tedious task. Agents can check links, find stale pages, compare claims, draft reports, and propose updates. But humans still need to set policy, review risky changes, and decide what the wiki is allowed to mean.&lt;/p&gt;

&lt;p&gt;That is the real bargain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;LLM Wiki maintenance is about keeping compiled knowledge honest.&lt;/p&gt;

&lt;p&gt;The important practices are not complicated: preserve sources, cite claims, keep indexes current, detect contradictions, review Git diffs, mark stale pages, and avoid broad unreviewed rewrites.&lt;/p&gt;

&lt;p&gt;A good LLM Wiki does not pretend to be magically self-correcting. It makes correction easier.&lt;/p&gt;

&lt;p&gt;That is the standard worth aiming for: not perfect knowledge, but inspectable knowledge that can be reviewed, repaired, and trusted over time.&lt;/p&gt;

</description>
      <category>knowledgemanagement</category>
      <category>wiki</category>
      <category>llm</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Syncthing File Sync for Self-Hosted Knowledge Systems</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:19:06 +0000</pubDate>
      <link>https://dev.to/rosgluk/syncthing-file-sync-for-self-hosted-knowledge-systems-44e7</link>
      <guid>https://dev.to/rosgluk/syncthing-file-sync-for-self-hosted-knowledge-systems-44e7</guid>
      <description>&lt;p&gt;Syncthing keeps files synchronized across devices you control, making it one of the most practical tools for a self-hosted knowledge infrastructure that avoids cloud lock-in.&lt;/p&gt;

&lt;p&gt;Unlike cloud storage platforms, Syncthing uses a peer-to-peer model where each device holds its own copy of synced folders and exchanges changes directly with trusted peers. There is no central server that owns your data, no subscription account, and no vendor lock-in. The project is open-source and community-driven, with more details at &lt;a href="https://syncthing.net" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This architecture makes Syncthing especially useful for knowledge workers who manage markdown notes, research documents, PDFs, and project files across a desktop, laptop, home server, and possibly a phone. The tool is simple in concept but requires careful setup to avoid common pitfalls like treating sync as backup or syncing folders that should remain isolated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Syncthing Is and Is Not
&lt;/h2&gt;

&lt;p&gt;Syncthing synchronizes files between two or more devices. Each device maintains its own copy of a folder, and changes propagate between trusted peers. Discovery and relay services may help devices find each other across networks, but the storage model remains local-first. The &lt;a href="https://docs.syncthing.net" rel="noopener noreferrer"&gt;Syncthing documentation&lt;/a&gt; covers installation and configuration in detail.&lt;/p&gt;

&lt;p&gt;The calm but important opinion is this: Syncthing is excellent when treated as sync infrastructure. It becomes dangerous when treated as backup.&lt;/p&gt;

&lt;p&gt;It is not a cloud drive. It is not a complete backup system. It is not a collaboration suite. It is a private, peer-to-peer file synchronization tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Syncthing Matters for Knowledge Management
&lt;/h2&gt;

&lt;p&gt;Knowledge management is not only about note-taking. It is also about where knowledge lives, how it moves, and whether it remains accessible over time — see the &lt;a href="https://www.glukhov.org/knowledge-management/" rel="noopener noreferrer"&gt;knowledge management guide&lt;/a&gt; for the broader picture of tools, methods, and self-hosted platforms this fits into.&lt;/p&gt;

&lt;p&gt;A useful personal or team knowledge system often contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;markdown notes&lt;/li&gt;
&lt;li&gt;PDFs and papers&lt;/li&gt;
&lt;li&gt;diagrams and screenshots&lt;/li&gt;
&lt;li&gt;exported web pages&lt;/li&gt;
&lt;li&gt;source snippets and configuration files&lt;/li&gt;
&lt;li&gt;meeting notes and project documents&lt;/li&gt;
&lt;li&gt;scanned documents and plain text logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these are just files. That is good. Files are durable, portable, searchable, scriptable, and easy to back up.&lt;/p&gt;

&lt;p&gt;Syncthing gives those files movement without forcing them into a vendor platform. You can write notes on one machine, read them on another, keep a copy on a home server, and still use normal tools like &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;ripgrep&lt;/code&gt;, &lt;a href="https://www.glukhov.org/knowledge-management/tools/obsidian-for-personal-knowledge-management/" rel="noopener noreferrer"&gt;Obsidian&lt;/a&gt;, VS Code, DokuWiki imports, static site generators, or custom scripts.&lt;/p&gt;

&lt;p&gt;For a self-hosted knowledge system, that is a strong architectural property. See &lt;a href="https://www.glukhov.org/knowledge-management/foundations/personal-knowledge-management/" rel="noopener noreferrer"&gt;personal knowledge management foundations&lt;/a&gt; for a broader view of PKM goals and methods, and &lt;a href="https://www.glukhov.org/knowledge-management/foundations/pkm-vs-rag-vs-wiki-vs-memory-systems/" rel="noopener noreferrer"&gt;PKM vs RAG vs Wiki vs Memory Systems&lt;/a&gt; for how different knowledge systems operate at different layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinct System Roles
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    S["Syncthing&amp;lt;br/&amp;gt;moves files"] --&amp;gt; B["Backup&amp;lt;br/&amp;gt;preserves history"]
    S --&amp;gt; K["Knowledge tools&amp;lt;br/&amp;gt;create and edit"]
    B --&amp;gt; R["Recovery&amp;lt;br/&amp;gt;when things go wrong"]
    K --&amp;gt; S
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep those roles separate.&lt;/p&gt;

&lt;p&gt;Syncthing should not be your only copy of important data. It should not be the only thing standing between you and accidental deletion. It should not be the only recovery mechanism after corruption, ransomware, filesystem failure, or a bad script.&lt;/p&gt;

&lt;p&gt;Used correctly, Syncthing is part of a resilient file workflow. Used alone, it can replicate mistakes very efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good Use Cases for Syncthing
&lt;/h2&gt;

&lt;p&gt;Syncthing works best when the folder has a clear owner, a predictable structure, and a small number of trusted devices.&lt;/p&gt;

&lt;p&gt;Good use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syncing an Obsidian or markdown notes vault&lt;/li&gt;
&lt;li&gt;Syncing project notes between desktop and laptop&lt;/li&gt;
&lt;li&gt;Syncing documents to an always-on home server&lt;/li&gt;
&lt;li&gt;Syncing exported PDFs and research material&lt;/li&gt;
&lt;li&gt;Syncing configuration files across personal machines&lt;/li&gt;
&lt;li&gt;Syncing scanned documents from one machine to another&lt;/li&gt;
&lt;li&gt;Syncing selected phone folders to a desktop or NAS&lt;/li&gt;
&lt;li&gt;Syncing static-site source notes before publishing&lt;/li&gt;
&lt;li&gt;Syncing knowledge archives between a workstation and server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not exotic use cases. They are exactly the kind of boring file movement that knowledge workers need every day.&lt;/p&gt;

&lt;p&gt;Syncthing is strongest when the folder remains understandable without Syncthing. If you can open the folder in a file manager and understand what it is, you are probably using the tool well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Risky Use Cases for Syncthing
&lt;/h2&gt;

&lt;p&gt;Syncthing becomes risky when users expect it to behave like backup, collaboration software, or managed cloud storage.&lt;/p&gt;

&lt;p&gt;Be careful with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using Syncthing as your only backup&lt;/li&gt;
&lt;li&gt;Syncing huge folders without thinking about deletion risk&lt;/li&gt;
&lt;li&gt;Syncing the same files while multiple apps edit them&lt;/li&gt;
&lt;li&gt;Syncing application databases that expect exclusive local access&lt;/li&gt;
&lt;li&gt;Syncing browser profiles&lt;/li&gt;
&lt;li&gt;Syncing mail stores&lt;/li&gt;
&lt;li&gt;Syncing build directories or cache folders&lt;/li&gt;
&lt;li&gt;Syncing folders with frequent generated files&lt;/li&gt;
&lt;li&gt;Syncing very large photo libraries to low-storage devices&lt;/li&gt;
&lt;li&gt;Relying on mobile background sync without testing it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue is not that Syncthing is unreliable. The issue is that sync is powerful. It does what you ask, including syncing deletions, conflicts, corrupted files, and accidental edits.&lt;/p&gt;

&lt;p&gt;That is why a knowledge system should combine Syncthing with versioning, snapshots, and real backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing Is Not Backup
&lt;/h2&gt;

&lt;p&gt;This point deserves its own section.&lt;/p&gt;

&lt;p&gt;Backup is about recovery. Sync is about convergence. These are related, but they are not the same.&lt;/p&gt;

&lt;p&gt;If you delete a file on one device, a sync tool may delete it everywhere. If a script corrupts a folder, the corrupted version may sync to other devices. If ransomware encrypts local files, the encrypted versions may be treated as changed files.&lt;/p&gt;

&lt;p&gt;File versioning can reduce this risk, but it does not turn Syncthing into a complete backup system. Syncthing versioning is configured per folder and per device, and it mainly protects old versions when changes are received from other devices. It does not magically preserve every local edit before it happens.&lt;/p&gt;

&lt;p&gt;A safer design is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Syncthing for active file movement.
File versioning for short-term mistake recovery.
Filesystem snapshots for local rollback.
Restic, Borg, ZFS send, Btrfs snapshots, or another backup system for real recovery.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more boring than pretending sync is backup. It is also much safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Knowledge Workflow
&lt;/h2&gt;

&lt;p&gt;A practical self-hosted knowledge setup can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    subgraph "Desktop Workstation"
        D["Main editing device&amp;lt;br/&amp;gt;Full notes and documents"]
    end
    subgraph "Laptop"
        L["Mobile editing device&amp;lt;br/&amp;gt;Same knowledge folders"]
    end
    subgraph "Home Server or NAS"
        H["Always-on sync target&amp;lt;br/&amp;gt;Receive-side versioning&amp;lt;br/&amp;gt;Filesystem snapshots&amp;lt;br/&amp;gt;Separate backup job"]
    end
    subgraph "Phone"
        P["Selected folders only&amp;lt;br/&amp;gt;Camera scans or quick capture&amp;lt;br/&amp;gt;Avoid full archive sync"]
    end
    D &amp;lt;--&amp;gt; H
    L &amp;lt;--&amp;gt; H
    P --&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shape works because the always-on server becomes the stable point in the system. It does not need to be a central cloud server, but it gives your sync topology a reliable anchor.&lt;/p&gt;

&lt;p&gt;For knowledge management, that anchor matters. Laptops sleep. Phones throttle background services. Desktops are not always on. A small home server or NAS gives the system somewhere steady to converge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folder Design Matters
&lt;/h2&gt;

&lt;p&gt;Do not create one giant "sync everything" folder.&lt;/p&gt;

&lt;p&gt;Create separate folders for separate purposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;knowledge-notes
knowledge-documents
research-papers
project-notes
scans-inbox
static-site-drafts
configs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you better control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which devices receive which files&lt;/li&gt;
&lt;li&gt;Which folders need versioning&lt;/li&gt;
&lt;li&gt;Which folders need snapshots&lt;/li&gt;
&lt;li&gt;Which folders can be send-only&lt;/li&gt;
&lt;li&gt;Which folders should avoid mobile devices&lt;/li&gt;
&lt;li&gt;Which folders have privacy or size concerns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Folder boundaries are architecture. They define trust, storage, recovery, and operational behavior.&lt;/p&gt;

&lt;p&gt;For example, an Obsidian vault might sync to laptop, desktop, and server. A large PDF archive might sync only to desktop and server. A phone scan inbox might be send-only from the phone to the server.&lt;/p&gt;

&lt;p&gt;That is much cleaner than treating every device as equal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Syncthing Folder Types
&lt;/h2&gt;

&lt;p&gt;Syncthing supports different folder behavior depending on how you want changes to flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send and receive&lt;/strong&gt; is the default mode. A device both sends local changes and receives remote changes. This is the normal choice for active editing across trusted devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send-only&lt;/strong&gt; sends local changes to other devices but does not accept remote changes as authoritative. This can be useful for source folders where one device should be treated as the origin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Receive-only&lt;/strong&gt; receives changes but does not publish local changes to the rest of the cluster. This can be useful for mirrors, replication targets, and backup-adjacent destinations where local edits should not be sent back.&lt;/p&gt;

&lt;p&gt;These modes are powerful, but they can also make the system harder to reason about. Use them intentionally. For most everyday folders, send and receive is simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Good Folder Layout for Notes
&lt;/h2&gt;

&lt;p&gt;For markdown-based notes, keep the structure predictable.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;knowledge-notes/
  inbox/
  projects/
  areas/
  references/
  archive/
  attachments/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well with tools like Obsidian, VS Code, ripgrep, static site generators, and command-line scripts.&lt;/p&gt;

&lt;p&gt;Avoid putting temporary exports, generated indexes, and application caches into the same folder unless you really want them synced. Use ignore patterns for files that are noisy or machine-specific.&lt;/p&gt;

&lt;p&gt;Examples of files you may want to ignore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.DS_Store
Thumbs.db
*.tmp
*.swp
.cache/
node_modules/
dist/
build/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Obsidian specifically, think carefully before syncing all plugin state. Some settings are useful across devices, but workspace state can be annoying if it constantly changes window layouts or active panes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing and Obsidian
&lt;/h2&gt;

&lt;p&gt;Syncthing is often used to sync Obsidian vaults because Obsidian stores notes as local markdown files.&lt;/p&gt;

&lt;p&gt;This is a good pairing. Obsidian gives you the writing and linking interface. Syncthing moves the files. Your notes remain plain files.&lt;/p&gt;

&lt;p&gt;The main risks are conflicts and mobile behavior.&lt;/p&gt;

&lt;p&gt;If you edit the same note on two devices before they sync, Syncthing may create conflict files. That is better than silent data loss, but it still requires cleanup. The practical habit is to let devices sync before editing the same active note elsewhere.&lt;/p&gt;

&lt;p&gt;For mobile devices, do not assume background sync behaves exactly like desktop sync. Test it. Open the app, let sync complete, edit a note, and verify the change appears on your other devices.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://www.glukhov.org/knowledge-management/tools/obsidian-vs-logseq-comparison/" rel="noopener noreferrer"&gt;Obsidian vs Logseq comparison&lt;/a&gt; for a broader discussion of sync considerations in PKM tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing and DokuWiki or Static Knowledge Sites
&lt;/h2&gt;

&lt;p&gt;Syncthing also works well around file-based or file-friendly knowledge systems.&lt;/p&gt;

&lt;p&gt;For DokuWiki, you might use Syncthing to move exported documents, media files, or staging content between machines. Be more careful with live server data, permissions, and concurrent edits. See &lt;a href="https://www.glukhov.org/knowledge-management/self-hosted-knowledge/dokuwiki-selfhosted-wiki-alternatives/" rel="noopener noreferrer"&gt;DokuWiki and self-hosted wiki alternatives&lt;/a&gt; for platform options.&lt;/p&gt;

&lt;p&gt;For Hugo or other static site generators, Syncthing can be useful for drafts, research notes, and content source files. It should not replace Git for source control, but it can complement Git for non-code knowledge material.&lt;/p&gt;

&lt;p&gt;A good rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use Git for history and collaboration.
Use Syncthing for private file movement.
Use backup for recovery.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool has a job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing vs Nextcloud
&lt;/h2&gt;

&lt;p&gt;Syncthing and Nextcloud are often compared, but they solve different problems.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Syncthing&lt;/th&gt;
&lt;th&gt;Nextcloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Peer-to-peer sync&lt;/td&gt;
&lt;td&gt;Central server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Private device-to-device folders&lt;/td&gt;
&lt;td&gt;Web-accessible file platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage model&lt;/td&gt;
&lt;td&gt;Local copies on devices&lt;/td&gt;
&lt;td&gt;Server-first storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;Minimal local admin UI&lt;/td&gt;
&lt;td&gt;Full web file interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharing links&lt;/td&gt;
&lt;td&gt;Not the main purpose&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Users and permissions&lt;/td&gt;
&lt;td&gt;Device trust model&lt;/td&gt;
&lt;td&gt;User and group model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendars and contacts&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Office collaboration&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Possible with add-ons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge notes&lt;/td&gt;
&lt;td&gt;Good for local-first notes&lt;/td&gt;
&lt;td&gt;Good for server-centered sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational style&lt;/td&gt;
&lt;td&gt;Lightweight but manual&lt;/td&gt;
&lt;td&gt;Heavier but more complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use Syncthing when you want private file sync between your own devices.&lt;/p&gt;

&lt;p&gt;Use Nextcloud when you want a self-hosted cloud platform with users, web access, sharing, calendars, contacts, and broader collaboration features. See the &lt;a href="https://www.glukhov.org/knowledge-management/self-hosted-knowledge/nextcloud/" rel="noopener noreferrer"&gt;Nextcloud self-hosting guide&lt;/a&gt; for a detailed setup walkthrough.&lt;/p&gt;

&lt;p&gt;Use both if you have both needs. For example, Syncthing can handle your local-first notes, while Nextcloud handles family file sharing or browser-based access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing vs rsync
&lt;/h2&gt;

&lt;p&gt;Rsync is excellent for one-off or scheduled file copying. It is simple, scriptable, and widely available.&lt;/p&gt;

&lt;p&gt;Syncthing is better when you want continuous synchronization across multiple devices without writing your own scheduling, conflict detection, and device discovery logic.&lt;/p&gt;

&lt;p&gt;Use rsync for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scripted deployments&lt;/li&gt;
&lt;li&gt;One-way copies&lt;/li&gt;
&lt;li&gt;Server maintenance&lt;/li&gt;
&lt;li&gt;Simple backup jobs&lt;/li&gt;
&lt;li&gt;Predictable batch transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Syncthing for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous multi-device sync&lt;/li&gt;
&lt;li&gt;Local-first notes&lt;/li&gt;
&lt;li&gt;Personal document movement&lt;/li&gt;
&lt;li&gt;Always-on folder convergence&lt;/li&gt;
&lt;li&gt;Cross-platform device sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rsync is a tool. Syncthing is a small synchronization system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing vs Seafile
&lt;/h2&gt;

&lt;p&gt;Seafile is closer to a file sync platform. It can be a good fit when you want a central service, clients, libraries, and a more managed team file sync experience.&lt;/p&gt;

&lt;p&gt;Syncthing is more decentralized and simpler in concept. There is no main server that owns the truth. Devices share folders with each other.&lt;/p&gt;

&lt;p&gt;For personal knowledge management, Syncthing is usually easier to reason about if you already like local files. For team file sharing, Seafile or Nextcloud may be more appropriate.&lt;/p&gt;

&lt;p&gt;The question is not which tool is better. The question is whether you want local-first peer sync or a central file platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android in 2026
&lt;/h2&gt;

&lt;p&gt;The Android story needs caution.&lt;/p&gt;

&lt;p&gt;The original Syncthing Android app was discontinued after the December 2024 release. Community forks and alternative approaches may exist, but Android should not be treated as the most stable part of a Syncthing knowledge system.&lt;/p&gt;

&lt;p&gt;This does not mean Syncthing is useless on Android. It means you should design mobile sync as a convenience layer, not the only reliable copy.&lt;/p&gt;

&lt;p&gt;For Android, prefer narrower use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan inbox&lt;/li&gt;
&lt;li&gt;Camera import folder&lt;/li&gt;
&lt;li&gt;Quick notes folder&lt;/li&gt;
&lt;li&gt;Read-only reference folder&lt;/li&gt;
&lt;li&gt;Selected documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid making your phone responsible for the only complete sync path of your knowledge archive. Phones are battery-managed, storage-limited, and increasingly restrictive about background file access.&lt;/p&gt;

&lt;p&gt;The boring recommendation is best: keep the authoritative knowledge set on desktop, laptop, and server. Let the phone participate selectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning Strategy
&lt;/h2&gt;

&lt;p&gt;Syncthing supports file versioning, and you should usually enable it on at least one stable device.&lt;/p&gt;

&lt;p&gt;For a knowledge-management setup, the home server or NAS is often the best place for versioning. It is always on, has more storage, and is easier to include in backup jobs.&lt;/p&gt;

&lt;p&gt;Common versioning approaches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trash can versioning&lt;/strong&gt; — moved files go to a trash folder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple versioning&lt;/strong&gt; — keeps a fixed number of old versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staggered versioning&lt;/strong&gt; — keeps more versions near the present, fewer as files age&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External versioning&lt;/strong&gt; — delegates versioning to an external tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most personal knowledge folders, staggered versioning is a reasonable starting point. It keeps more versions near the present and fewer versions as files get older.&lt;/p&gt;

&lt;p&gt;Versioning is not free. It consumes storage and needs occasional review. But storage is cheaper than losing a year of notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshots and Backup
&lt;/h2&gt;

&lt;p&gt;If your Syncthing target is a NAS or Linux server, add snapshots.&lt;/p&gt;

&lt;p&gt;Good options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ZFS snapshots&lt;/li&gt;
&lt;li&gt;Btrfs snapshots&lt;/li&gt;
&lt;li&gt;LVM snapshots&lt;/li&gt;
&lt;li&gt;restic&lt;/li&gt;
&lt;li&gt;BorgBackup&lt;/li&gt;
&lt;li&gt;Kopia&lt;/li&gt;
&lt;li&gt;Filesystem-level backup tools from your NAS platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A strong setup might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Syncthing syncs files to the server.
The server keeps Syncthing file versions.
The filesystem keeps snapshots.
A backup tool copies encrypted backups off-device.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you multiple recovery layers.&lt;/p&gt;

&lt;p&gt;If you accidentally delete a note, Syncthing versioning may help. If a folder is corrupted, filesystem snapshots may help. If the server disk dies, off-device backup may help.&lt;/p&gt;

&lt;p&gt;That is the difference between a sync setup and a recovery strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and Trust
&lt;/h2&gt;

&lt;p&gt;Syncthing is attractive because it lets you avoid placing all knowledge files into a third-party cloud account.&lt;/p&gt;

&lt;p&gt;However, privacy is not automatic. You still need to think about device trust.&lt;/p&gt;

&lt;p&gt;Every normal trusted device that participates in a folder can read that folder. If you sync your notes to an old laptop, that laptop is now part of your security boundary. If you sync documents to a VPS, that server matters too.&lt;/p&gt;

&lt;p&gt;Syncthing also has an untrusted encrypted device feature, but treat it carefully. It can be useful when you want an encrypted replica on a device that should not see plaintext file content, but it is more advanced and should be tested before relying on it.&lt;/p&gt;

&lt;p&gt;For most people, the simpler model is better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only sync sensitive folders to devices you actually trust.
Encrypt disks on laptops and servers.
Back up important data separately.
Do not sync private archives everywhere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-hosted does not automatically mean secure. It means you own the responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflict Handling
&lt;/h2&gt;

&lt;p&gt;Conflicts happen when different devices change the same file before synchronization converges.&lt;/p&gt;

&lt;p&gt;For notes, this can happen if you edit the same markdown file on a laptop and desktop while one of them is offline. Syncthing will preserve conflict copies rather than silently choosing one version.&lt;/p&gt;

&lt;p&gt;That is the right behavior, but it still leaves you with cleanup work.&lt;/p&gt;

&lt;p&gt;To reduce conflicts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid editing the same note on two offline devices&lt;/li&gt;
&lt;li&gt;Let sync finish before switching machines&lt;/li&gt;
&lt;li&gt;Keep frequently edited inbox notes small&lt;/li&gt;
&lt;li&gt;Avoid syncing application state files unnecessarily&lt;/li&gt;
&lt;li&gt;Use ignore patterns for volatile files&lt;/li&gt;
&lt;li&gt;Review conflict files periodically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conflict files are not a sign that Syncthing is broken. They are a sign that two devices changed reality at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignore Patterns
&lt;/h2&gt;

&lt;p&gt;Ignore patterns are important for knowledge folders.&lt;/p&gt;

&lt;p&gt;They keep generated, temporary, or machine-specific files out of the sync set. This reduces conflicts, saves bandwidth, and avoids polluting other devices.&lt;/p&gt;

&lt;p&gt;Common examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.DS_Store
Thumbs.db
*.tmp
*.swp
*.bak
.cache/
node_modules/
dist/
build/
__pycache__/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a notes vault, consider whether plugin caches, workspace layout files, or generated indexes should really sync. Some should. Some should not.&lt;/p&gt;

&lt;p&gt;The principle is simple: sync source material, not noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suggested Syncthing Topologies
&lt;/h2&gt;

&lt;p&gt;For one person, a star-like topology around a home server is often easiest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;desktop &amp;lt;-&amp;gt; home-server
laptop  &amp;lt;-&amp;gt; home-server
phone   -&amp;gt; home-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The devices can still connect directly, but the server gives them a stable meeting point.&lt;/p&gt;

&lt;p&gt;For a small household, keep personal folders separate. Do not create one shared mega-folder unless everyone understands the consequences.&lt;/p&gt;

&lt;p&gt;For a small technical team, be careful. Syncthing can work for shared files among trusted peers, but it does not replace proper collaboration tools, permissions, review workflows, or version control.&lt;/p&gt;

&lt;p&gt;The more people you add, the more attractive a server-centered platform like Nextcloud, Seafile, Git, or a document system becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Setup Example
&lt;/h2&gt;

&lt;p&gt;Here is a reasonable self-hosted knowledge setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Folders:
  knowledge-notes
  knowledge-documents
  research-papers
  scans-inbox

Devices:
  desktop
  laptop
  home-server
  phone

Rules:
  desktop and laptop use send-receive for notes
  home-server uses send-receive with versioning enabled
  phone sends scans-inbox only
  large PDFs do not sync to phone
  home-server snapshots all synced folders
  home-server backup runs nightly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup is not fancy, but it is robust.&lt;/p&gt;

&lt;p&gt;It supports local-first work, mobile capture, server-side recovery, and off-device backup. It avoids making the phone responsible for the whole archive. It also avoids confusing sync with backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Checklist
&lt;/h2&gt;

&lt;p&gt;Before trusting Syncthing with important knowledge files, check the basics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are all important folders synced to at least two real devices?
Is one device always on or frequently online?
Is file versioning enabled on at least one stable device?
Are filesystem snapshots enabled on the server or NAS?
Is there an off-device backup?
Are noisy files ignored?
Are mobile folders limited?
Have you tested restore?
Have you tested conflict behavior?
Do you understand which folders are send-only or receive-only?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important item is restore testing.&lt;/p&gt;

&lt;p&gt;A backup strategy you have never restored from is a theory. A sync strategy you have never tested under conflict is also a theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;The most common mistake is syncing too much.&lt;/p&gt;

&lt;p&gt;People start with one useful folder, then add every document, every photo, every export, every cache, and every application directory. The system becomes noisy and hard to reason about.&lt;/p&gt;

&lt;p&gt;The second mistake is treating receive-only folders as magical backup. They are useful, but they are not a full historical recovery system.&lt;/p&gt;

&lt;p&gt;The third mistake is ignoring deletion behavior. If deletion syncs everywhere, then deletion is part of the design.&lt;/p&gt;

&lt;p&gt;The fourth mistake is trusting mobile sync too much. Mobile operating systems are not friendly to long-running background file synchronization.&lt;/p&gt;

&lt;p&gt;The fifth mistake is not using versioning. If the data matters, keep versions somewhere.&lt;/p&gt;

&lt;p&gt;The sixth mistake is not having a real backup. Syncthing can help populate a backup target, but it should not be the only protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Syncthing Fits in a Self-Hosted Knowledge Stack
&lt;/h2&gt;

&lt;p&gt;In a self-hosted knowledge system, Syncthing is infrastructure.&lt;/p&gt;

&lt;p&gt;It sits below note-taking apps, wikis, search tools, static sites, scripts, and AI-assisted workflows. It moves the files those systems use.&lt;/p&gt;

&lt;p&gt;A simple stack might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TB
    subgraph "Capture"
        C1["Phone scanner"]
        C2["Browser save"]
        C3["Quick notes"]
    end
    subgraph "Sync"
        S["Syncthing"]
    end
    subgraph "Storage"
        ST1["Desktop"]
        ST2["Laptop"]
        ST3["Home server / NAS"]
    end
    subgraph "Authoring"
        A1["Obsidian"]
        A2["VS Code"]
        A3["Vim"]
        A4["DokuWiki"]
        A5["Static site generator"]
    end
    subgraph "Search"
        SR1["ripgrep"]
        SR2["Desktop search"]
        SR3["Local index"]
        SR4["RAG pipeline"]
    end
    subgraph "Recovery"
        R1["Versioning"]
        R2["Snapshots"]
        R3["Encrypted backup"]
    end
    C1 --&amp;gt; S
    C2 --&amp;gt; S
    C3 --&amp;gt; S
    S --&amp;gt; ST1
    S --&amp;gt; ST2
    S --&amp;gt; ST3
    ST1 --&amp;gt; A1
    ST1 --&amp;gt; A2
    ST2 --&amp;gt; A3
    ST3 --&amp;gt; A4
    ST3 --&amp;gt; A5
    A1 --&amp;gt; SR1
    A2 --&amp;gt; SR2
    A3 --&amp;gt; SR3
    A4 --&amp;gt; SR4
    ST3 --&amp;gt; R1
    ST3 --&amp;gt; R2
    ST3 --&amp;gt; R3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is healthy.&lt;/p&gt;

&lt;p&gt;You can replace the editor without replacing the storage. You can replace the backup tool without replacing the note format. You can stop Syncthing and still have ordinary files.&lt;/p&gt;

&lt;p&gt;That is the value of local-first knowledge infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Syncthing Is the Right Tool
&lt;/h2&gt;

&lt;p&gt;Syncthing is a strong choice when the problem is private file synchronization between devices you control. It is less suitable when the real problem is collaboration, web access, permissions, or long-term recovery.&lt;/p&gt;

&lt;p&gt;This distinction matters because many self-hosted file workflows fail from unclear expectations. A sync tool can move files very well, but it should not be asked to behave like a cloud suite, a backup archive, and a team collaboration platform at the same time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Syncthing fit&lt;/th&gt;
&lt;th&gt;Better fit when Syncthing is not enough&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Private sync between your own devices&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Usually none needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local-first markdown notes&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Obsidian Sync if you prefer managed sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal document folders&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Nextcloud if you need browser access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Family file sharing&lt;/td&gt;
&lt;td&gt;Possible, but awkward&lt;/td&gt;
&lt;td&gt;Nextcloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team document collaboration&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Nextcloud, Seafile, Google Drive, Microsoft 365&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-way server copy&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;rsync may be simpler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full backup and restore&lt;/td&gt;
&lt;td&gt;Not enough alone&lt;/td&gt;
&lt;td&gt;restic, BorgBackup, Kopia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web file access&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Nextcloud or Seafile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar and contacts&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Nextcloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile photo backup&lt;/td&gt;
&lt;td&gt;Possible with caution&lt;/td&gt;
&lt;td&gt;Immich, Nextcloud, platform photo tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted offsite backup&lt;/td&gt;
&lt;td&gt;Not the main role&lt;/td&gt;
&lt;td&gt;restic, BorgBackup, Kopia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted sync to untrusted device&lt;/td&gt;
&lt;td&gt;Advanced use case&lt;/td&gt;
&lt;td&gt;Syncthing untrusted devices, tested carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the main reason Syncthing remains useful. It does not try to become a full cloud platform. It works best when the job is clear: keep selected folders synchronized across trusted devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendation
&lt;/h2&gt;

&lt;p&gt;Use Syncthing for self-hosted knowledge management when you want private, local-first file sync across trusted devices.&lt;/p&gt;

&lt;p&gt;Use it for notes, documents, research folders, scans, and personal knowledge archives. Pair it with markdown, plain files, local search, and self-hosted services. Keep the structure simple enough that you can understand it without a dashboard.&lt;/p&gt;

&lt;p&gt;But do not confuse sync with backup.&lt;/p&gt;

&lt;p&gt;The best setup is not Syncthing alone. The best setup is Syncthing plus file versioning, filesystem snapshots, and real backup.&lt;/p&gt;

&lt;p&gt;That combination gives you the thing self-hosted knowledge systems should aim for: local control, practical convenience, and recoverable data.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>backup</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Circuit Breaker Pattern in Go: Stop Cascading Failures</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 18 Jul 2026 07:49:30 +0000</pubDate>
      <link>https://dev.to/rosgluk/circuit-breaker-pattern-in-go-stop-cascading-failures-7cg</link>
      <guid>https://dev.to/rosgluk/circuit-breaker-pattern-in-go-stop-cascading-failures-7cg</guid>
      <description>&lt;p&gt;A circuit breaker stops your Go service from hammering a failing dependency,&lt;br&gt;
preventing cascading failures that consume goroutines, sockets, and memory until the entire system collapses.&lt;/p&gt;



&lt;p&gt;The hard part is not the state machine. It is deciding where the breaker belongs, what counts as failure, how it interacts with timeouts and retries, and what your service should do when the circuit is open.&lt;/p&gt;

&lt;p&gt;In Go, the circuit breaker pattern is especially useful around outbound calls: HTTP APIs, payment gateways, search services, email providers, LLM gateways, internal microservices, and other dependencies that can become slow, overloaded, or partially unavailable. Used well, a circuit breaker reduces cascading failures. Used badly, it becomes another obscure failure mode.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Problem Does a Circuit Breaker Solve?
&lt;/h2&gt;

&lt;p&gt;Distributed systems rarely fail cleanly.&lt;/p&gt;

&lt;p&gt;A dependency might not be fully down. It might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;returning 500 errors&lt;/li&gt;
&lt;li&gt;returning 429 rate limit responses&lt;/li&gt;
&lt;li&gt;accepting TCP connections but never replying&lt;/li&gt;
&lt;li&gt;responding in 30 seconds instead of 300 milliseconds&lt;/li&gt;
&lt;li&gt;failing only for some requests&lt;/li&gt;
&lt;li&gt;overloaded because every client is retrying at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst case is often not a hard failure. It is a slow dependency.&lt;/p&gt;

&lt;p&gt;Slow calls consume goroutines, sockets, database connections, memory, and worker capacity. If your service keeps waiting on a dependency that is already unhealthy, your service can become unhealthy too.&lt;/p&gt;

&lt;p&gt;A circuit breaker prevents that by failing fast after the dependency crosses a failure threshold.&lt;/p&gt;

&lt;p&gt;Instead of doing this forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request -&amp;gt; call dependency -&amp;gt; wait -&amp;gt; timeout -&amp;gt; retry -&amp;gt; wait -&amp;gt; fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the service eventually does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request -&amp;gt; circuit open -&amp;gt; return fallback or error immediately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fast failure is not always pleasant, but it is predictable. Predictable failure is easier to operate than a slow collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Circuit Breaker States
&lt;/h2&gt;

&lt;p&gt;Most circuit breakers use three states.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closed
&lt;/h3&gt;

&lt;p&gt;The circuit is closed during normal operation.&lt;/p&gt;

&lt;p&gt;Requests are allowed through. The breaker records successes and failures. If the number or ratio of failures crosses a threshold, the breaker opens.&lt;/p&gt;

&lt;p&gt;Closed does not mean "safe forever." It means "traffic is currently allowed."&lt;/p&gt;

&lt;h3&gt;
  
  
  Open
&lt;/h3&gt;

&lt;p&gt;The circuit is open when the dependency is considered unhealthy.&lt;/p&gt;

&lt;p&gt;Requests are rejected immediately. The service should return a fallback, cached response, degraded response, or a clear upstream error.&lt;/p&gt;

&lt;p&gt;Open does not fix the dependency. It gives the dependency time to recover and protects the caller from wasting resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Half-Open
&lt;/h3&gt;

&lt;p&gt;After a cool-down period, the breaker enters a half-open state.&lt;/p&gt;

&lt;p&gt;Only a limited number of trial requests are allowed through. If they succeed, the breaker closes. If they fail, the breaker opens again.&lt;/p&gt;

&lt;p&gt;Half-open is important because it avoids two bad extremes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never trying the dependency again&lt;/li&gt;
&lt;li&gt;sending full traffic back too quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The state transitions look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Closed
    Closed --&amp;gt; Open: Failure threshold reached
    Open --&amp;gt; HalfOpen: Timeout elapsed
    HalfOpen --&amp;gt; Closed: Trial succeeds
    HalfOpen --&amp;gt; Open: Trial fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Circuit Breaker vs Timeout vs Retry
&lt;/h2&gt;

&lt;p&gt;A common mistake is treating circuit breakers, retries, and timeouts as interchangeable. They are related, but they solve different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout
&lt;/h3&gt;

&lt;p&gt;A timeout limits how long one operation can run.&lt;/p&gt;

&lt;p&gt;In Go, this usually means passing a &lt;code&gt;context.Context&lt;/code&gt; with a deadline or timeout into the outbound call.&lt;/p&gt;

&lt;p&gt;A timeout answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How long am I willing to wait for this one call?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Retry
&lt;/h3&gt;

&lt;p&gt;A retry repeats an operation when the failure might be temporary.&lt;/p&gt;

&lt;p&gt;Retries are useful for short network glitches, temporary 503 responses, connection resets, and other transient failures.&lt;/p&gt;

&lt;p&gt;A retry answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should I try this call again?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Circuit Breaker
&lt;/h3&gt;

&lt;p&gt;A circuit breaker stops calls when the dependency is probably unhealthy.&lt;/p&gt;

&lt;p&gt;It answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should I call this dependency at all right now?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rate Limiter
&lt;/h3&gt;

&lt;p&gt;A rate limiter controls how much traffic is allowed over time.&lt;/p&gt;

&lt;p&gt;It answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How much traffic should this caller send?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bulkhead
&lt;/h3&gt;

&lt;p&gt;A bulkhead isolates resources so one dependency cannot consume everything.&lt;/p&gt;

&lt;p&gt;It answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How much of my service can this dependency damage?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These patterns are strongest when used together. A circuit breaker without timeouts is weak. Retries without jitter can create retry storms. A fallback without metrics can hide an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use a Circuit Breaker in Go
&lt;/h2&gt;

&lt;p&gt;Use a circuit breaker when your service calls a dependency that can fail independently from your service.&lt;/p&gt;

&lt;p&gt;Good candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;external HTTP APIs&lt;/li&gt;
&lt;li&gt;payment processors&lt;/li&gt;
&lt;li&gt;email and SMS providers&lt;/li&gt;
&lt;li&gt;search services&lt;/li&gt;
&lt;li&gt;recommendation services&lt;/li&gt;
&lt;li&gt;LLM inference gateways&lt;/li&gt;
&lt;li&gt;internal microservice endpoints&lt;/li&gt;
&lt;li&gt;third-party SaaS APIs&lt;/li&gt;
&lt;li&gt;slow or overloaded read-side services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Circuit breakers are especially useful when the caller can degrade gracefully.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return cached product data&lt;/li&gt;
&lt;li&gt;skip a recommendation block&lt;/li&gt;
&lt;li&gt;mark a payment provider as temporarily unavailable&lt;/li&gt;
&lt;li&gt;queue work for later&lt;/li&gt;
&lt;li&gt;return a partial response&lt;/li&gt;
&lt;li&gt;fail fast with a clear temporary error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important question is not "can this call fail?" Everything can fail. The better question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If this dependency is failing, should we continue sending full traffic to it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the answer is no, a circuit breaker may help.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use a Circuit Breaker
&lt;/h2&gt;

&lt;p&gt;Do not add a circuit breaker to every function just because the pattern sounds responsible.&lt;/p&gt;

&lt;p&gt;A circuit breaker is usually not useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local in-process function calls&lt;/li&gt;
&lt;li&gt;simple CRUD inside a monolith&lt;/li&gt;
&lt;li&gt;validation logic&lt;/li&gt;
&lt;li&gt;deterministic business rules&lt;/li&gt;
&lt;li&gt;CPU-only local operations&lt;/li&gt;
&lt;li&gt;code paths where no useful fallback exists&lt;/li&gt;
&lt;li&gt;write operations that are not idempotent&lt;/li&gt;
&lt;li&gt;dependencies already protected by a stronger workflow layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A circuit breaker also does not replace basic hygiene:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set timeouts&lt;/li&gt;
&lt;li&gt;propagate context&lt;/li&gt;
&lt;li&gt;use connection pools correctly&lt;/li&gt;
&lt;li&gt;handle errors explicitly&lt;/li&gt;
&lt;li&gt;make retries safe&lt;/li&gt;
&lt;li&gt;observe failure rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bad circuit breaker can make a system harder to reason about. It can hide the real problem, reject traffic too aggressively, or create confusing behavior during recovery.&lt;/p&gt;

&lt;p&gt;The slightly opinionated rule is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add circuit breakers at dependency boundaries, not everywhere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Choosing a Go Circuit Breaker Library
&lt;/h2&gt;

&lt;p&gt;You can implement a basic circuit breaker yourself, but most production Go services should use a library.&lt;/p&gt;

&lt;p&gt;The most common simple choice is &lt;code&gt;sony/gobreaker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;closed, open, and half-open states&lt;/li&gt;
&lt;li&gt;configurable failure thresholds&lt;/li&gt;
&lt;li&gt;configurable open-state timeout&lt;/li&gt;
&lt;li&gt;state change callbacks&lt;/li&gt;
&lt;li&gt;request counters&lt;/li&gt;
&lt;li&gt;generic support in v2&lt;/li&gt;
&lt;li&gt;a small API surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For larger resilience pipelines, you may also look at libraries that compose multiple policies, such as retry, timeout, fallback, rate limiting, bulkhead isolation, and circuit breaking. That can be useful when you want a single resilience layer around an operation.&lt;/p&gt;

&lt;p&gt;For many Go services, though, &lt;code&gt;gobreaker&lt;/code&gt; is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Circuit Breaker Packages Compared
&lt;/h2&gt;

&lt;p&gt;Go does not include a built-in circuit breaker in the standard library. In practice, you usually choose between a small circuit breaker library, a larger resilience framework, or an older Hystrix-style package.&lt;/p&gt;

&lt;p&gt;For most new Go services, the decision is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;sony/gobreaker&lt;/code&gt; if you want a small, focused circuit breaker&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;failsafe-go&lt;/code&gt; if you want circuit breakers composed with retries, timeouts, fallbacks, bulkheads, rate limits, and other resilience policies&lt;/li&gt;
&lt;li&gt;avoid starting new projects on &lt;code&gt;hystrix-go&lt;/code&gt; unless you already have legacy code using it&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Tradeoffs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sony/gobreaker/v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Simple circuit breakers around HTTP/RPC clients&lt;/td&gt;
&lt;td&gt;Small API, generic v2 support, clear state model, easy to wrap dependency clients&lt;/td&gt;
&lt;td&gt;Only solves circuit breaking; retries, timeouts, and fallbacks must be composed separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failsafe-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full resilience policy composition&lt;/td&gt;
&lt;td&gt;Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter, and adaptive throttler policies&lt;/td&gt;
&lt;td&gt;More concepts to learn; heavier than needed if you only want a basic breaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;afex/hystrix-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy Hystrix-style systems&lt;/td&gt;
&lt;td&gt;Familiar Hystrix concepts, command-style execution, historical usage&lt;/td&gt;
&lt;td&gt;Older design; not the best default for new Go services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go-kit/kit/circuitbreaker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go kit endpoint-based services&lt;/td&gt;
&lt;td&gt;Fits Go kit middleware style and endpoint architecture&lt;/td&gt;
&lt;td&gt;Mostly useful if your service already uses Go kit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cep21/circuit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hystrix-like circuit breaker behavior&lt;/td&gt;
&lt;td&gt;More featureful Hystrix-style approach&lt;/td&gt;
&lt;td&gt;Less common as the simple default; may be more than needed for small services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My default recommendation is boring on purpose: start with &lt;code&gt;sony/gobreaker/v2&lt;/code&gt; when you only need a circuit breaker. Reach for &lt;code&gt;failsafe-go&lt;/code&gt; when you want to express a complete resilience policy in one place.&lt;/p&gt;

&lt;p&gt;That split keeps the architecture clean. A small service client does not need a full resilience framework just to stop calling a failing dependency. But a gateway, aggregator, API client SDK, or high-traffic integration layer may benefit from composed policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing gobreaker
&lt;/h2&gt;

&lt;p&gt;Use the v2 package for new code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/sony/gobreaker/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/sony/gobreaker/v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Basic Circuit Breaker in Go
&lt;/h2&gt;

&lt;p&gt;Here is a small example around an HTTP call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/sony/gobreaker/v2"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ErrTemporaryUnavailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dependency temporarily unavailable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserClient&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;cb&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;[[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewUserClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"user-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MaxRequests&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Interval&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ReadyToTrip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConsecutiveFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;OnStateChange&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"circuit breaker %s changed from %s to %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;UserClient&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;[[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;settings&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/users/"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user service returned %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user service client error: %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrOpenState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrTemporaryUnavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrTooManyRequests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrTemporaryUnavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a complete production client, but it shows the shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the breaker wraps the outbound call&lt;/li&gt;
&lt;li&gt;the HTTP request receives a context&lt;/li&gt;
&lt;li&gt;the HTTP client has a timeout&lt;/li&gt;
&lt;li&gt;server-side failures count as breaker failures&lt;/li&gt;
&lt;li&gt;open circuit errors are mapped into an application error&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring gobreaker Settings
&lt;/h2&gt;

&lt;p&gt;The key settings are worth understanding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Name
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Name&lt;/code&gt; identifies the breaker.&lt;/p&gt;

&lt;p&gt;Use a stable, specific name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment-api
search-service
llm-gateway
user-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid vague names like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http-client
external-call
default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will want this name in logs and metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  MaxRequests
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MaxRequests&lt;/code&gt; controls how many requests are allowed while the breaker is half-open.&lt;/p&gt;

&lt;p&gt;A small number is usually safer. The purpose of half-open is to test recovery, not to send full traffic immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interval
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Interval&lt;/code&gt; controls when internal counts are cleared while the breaker is closed.&lt;/p&gt;

&lt;p&gt;If it is zero, counts are not cleared automatically. A non-zero interval gives the breaker a rolling-ish memory window, although it is not the same as a full sliding window implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Timeout&lt;/code&gt; controls how long the breaker stays open before moving to half-open.&lt;/p&gt;

&lt;p&gt;If the timeout is too short, your service will keep probing a dependency that has not recovered. If it is too long, recovery will be delayed.&lt;/p&gt;

&lt;p&gt;Start with something conservative, such as 10 to 30 seconds, then tune from production metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  ReadyToTrip
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReadyToTrip&lt;/code&gt; decides when the breaker should open.&lt;/p&gt;

&lt;p&gt;A simple rule is consecutive failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ReadyToTrip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConsecutiveFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is easy to reason about, but it may not be right for high-volume services.&lt;/p&gt;

&lt;p&gt;Another option is failure ratio after a minimum number of requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ReadyToTrip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Requests&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalFailures&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids opening the circuit after a tiny sample size.&lt;/p&gt;

&lt;h3&gt;
  
  
  OnStateChange
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;OnStateChange&lt;/code&gt; is where you should emit logs or metrics.&lt;/p&gt;

&lt;p&gt;At minimum, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;breaker name&lt;/li&gt;
&lt;li&gt;old state&lt;/li&gt;
&lt;li&gt;new state&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production systems, expose breaker state as a metric. Logs are useful for debugging, but metrics are better for alerting and dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  IsSuccessful
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IsSuccessful&lt;/code&gt; lets you decide which errors count as failures.&lt;/p&gt;

&lt;p&gt;This is important.&lt;/p&gt;

&lt;p&gt;Not every error should open the breaker. For example, a &lt;code&gt;404 Not Found&lt;/code&gt; from a user service may be a valid business result. A &lt;code&gt;400 Bad Request&lt;/code&gt; might be the caller's fault, not the dependency's fault.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;503 Service Unavailable&lt;/code&gt;, timeout, connection reset, or &lt;code&gt;429 Too Many Requests&lt;/code&gt; may be a real dependency health signal.&lt;/p&gt;

&lt;p&gt;Be careful here. Counting the wrong errors is one of the easiest ways to build a noisy circuit breaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Count as Failure?
&lt;/h2&gt;

&lt;p&gt;This is where engineering judgement matters.&lt;/p&gt;

&lt;p&gt;Usually count these as failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network timeouts&lt;/li&gt;
&lt;li&gt;connection refused&lt;/li&gt;
&lt;li&gt;connection reset&lt;/li&gt;
&lt;li&gt;HTTP 500&lt;/li&gt;
&lt;li&gt;HTTP 502&lt;/li&gt;
&lt;li&gt;HTTP 503&lt;/li&gt;
&lt;li&gt;HTTP 504&lt;/li&gt;
&lt;li&gt;repeated 429 responses&lt;/li&gt;
&lt;li&gt;malformed responses from the dependency&lt;/li&gt;
&lt;li&gt;context deadline exceeded during the outbound call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usually do not count these as dependency failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validation errors&lt;/li&gt;
&lt;li&gt;local serialization errors&lt;/li&gt;
&lt;li&gt;expected 404 responses&lt;/li&gt;
&lt;li&gt;caller-side authorization failures&lt;/li&gt;
&lt;li&gt;business rule rejections&lt;/li&gt;
&lt;li&gt;user input errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The breaker should represent dependency health, not general application failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and context.Context
&lt;/h2&gt;

&lt;p&gt;In Go, circuit breakers should not replace &lt;code&gt;context.Context&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A circuit breaker decides whether to attempt a call. A context controls how long that call may run and whether it should stop when the caller is gone.&lt;/p&gt;

&lt;p&gt;A good outbound call should usually have both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parentCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context should flow through the call chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;incoming request context
-&amp;gt; service method
-&amp;gt; client method
-&amp;gt; HTTP request
-&amp;gt; dependency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid creating detached background contexts inside request-scoped code. If the user request is canceled, the downstream work should usually stop too.&lt;/p&gt;

&lt;p&gt;The calm rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The breaker protects the system. The context protects the request.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You normally need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Retries
&lt;/h2&gt;

&lt;p&gt;Retries and circuit breakers can work well together, but the order matters.&lt;/p&gt;

&lt;p&gt;The safest default is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timeout per attempt
retry with backoff and jitter
circuit breaker around the dependency call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there is no universal answer. Think about what you want to count.&lt;/p&gt;

&lt;p&gt;If each retry attempt passes through the breaker, one user request can contribute multiple failures. That may open the breaker faster, which can be good or bad.&lt;/p&gt;

&lt;p&gt;If the breaker wraps the whole retry operation, the breaker sees one final success or failure per user request. That is calmer, but it may hide the number of failed attempts.&lt;/p&gt;

&lt;p&gt;For many application services, this shape is reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user request
-&amp;gt; circuit breaker
   -&amp;gt; retry policy
      -&amp;gt; one HTTP attempt with timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the breaker tracks whether the dependency operation ultimately worked for the caller.&lt;/p&gt;

&lt;p&gt;For lower-level clients, this shape can also make sense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user request
-&amp;gt; retry policy
   -&amp;gt; circuit breaker
      -&amp;gt; one HTTP attempt with timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the breaker protects each attempt.&lt;/p&gt;

&lt;p&gt;The more important rule is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not retry blindly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a small maximum retry count&lt;/li&gt;
&lt;li&gt;exponential backoff&lt;/li&gt;
&lt;li&gt;jitter&lt;/li&gt;
&lt;li&gt;per-attempt timeouts&lt;/li&gt;
&lt;li&gt;an overall request deadline&lt;/li&gt;
&lt;li&gt;idempotency for writes&lt;/li&gt;
&lt;li&gt;metrics for retry attempts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those, retries can turn a small outage into a larger one. For a deeper treatment of retry safety, see &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems That Actually Works&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Idempotency
&lt;/h2&gt;

&lt;p&gt;Circuit breakers often appear next to retries, and retries raise the question of idempotency.&lt;/p&gt;

&lt;p&gt;For read operations, retrying is usually safe.&lt;/p&gt;

&lt;p&gt;For write operations, retrying can be dangerous.&lt;/p&gt;

&lt;p&gt;Consider this payment call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /charge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request times out, did the payment fail? Maybe. Did it succeed but the response was lost? Also maybe.&lt;/p&gt;

&lt;p&gt;If you retry without an idempotency key, you might charge twice.&lt;/p&gt;

&lt;p&gt;For write operations, use one or more of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;request IDs&lt;/li&gt;
&lt;li&gt;operation IDs&lt;/li&gt;
&lt;li&gt;unique constraints&lt;/li&gt;
&lt;li&gt;transactional outbox&lt;/li&gt;
&lt;li&gt;workflow orchestration&lt;/li&gt;
&lt;li&gt;explicit reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A circuit breaker can stop you from continuing to call a failing payment provider, but it cannot make unsafe retries safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Fallbacks
&lt;/h2&gt;

&lt;p&gt;When the circuit is open, your service needs a plan.&lt;/p&gt;

&lt;p&gt;Possible fallback strategies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return cached data&lt;/li&gt;
&lt;li&gt;return stale data with a warning&lt;/li&gt;
&lt;li&gt;omit a non-critical section&lt;/li&gt;
&lt;li&gt;queue work for later&lt;/li&gt;
&lt;li&gt;switch to another provider&lt;/li&gt;
&lt;li&gt;return a temporary error&lt;/li&gt;
&lt;li&gt;show degraded functionality&lt;/li&gt;
&lt;li&gt;fail the request quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fallback should be honest.&lt;/p&gt;

&lt;p&gt;For example, this is usually good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"temporary_unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recommendations are temporarily unavailable"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is risky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty list may look like a valid result. It can hide an outage, confuse users, and make debugging harder.&lt;/p&gt;

&lt;p&gt;Silent fallbacks are tempting. They are also dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Observability
&lt;/h2&gt;

&lt;p&gt;A circuit breaker without observability is mostly a surprise generator.&lt;/p&gt;

&lt;p&gt;Track at least these metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current breaker state&lt;/li&gt;
&lt;li&gt;state changes&lt;/li&gt;
&lt;li&gt;calls allowed&lt;/li&gt;
&lt;li&gt;calls rejected&lt;/li&gt;
&lt;li&gt;successes&lt;/li&gt;
&lt;li&gt;failures&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;fallback responses&lt;/li&gt;
&lt;li&gt;retry attempts&lt;/li&gt;
&lt;li&gt;downstream latency&lt;/li&gt;
&lt;li&gt;downstream status codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful labels include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;breaker name&lt;/li&gt;
&lt;li&gt;dependency name&lt;/li&gt;
&lt;li&gt;operation name&lt;/li&gt;
&lt;li&gt;status class&lt;/li&gt;
&lt;li&gt;error category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid high-cardinality labels such as user ID, full URL, request ID, or raw error messages.&lt;/p&gt;

&lt;p&gt;You should be able to answer these questions from dashboards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which circuit breakers are open right now?&lt;/li&gt;
&lt;li&gt;How often do they open?&lt;/li&gt;
&lt;li&gt;Which dependency caused the opening?&lt;/li&gt;
&lt;li&gt;Are users seeing fallback responses?&lt;/li&gt;
&lt;li&gt;Did latency improve after the breaker opened?&lt;/li&gt;
&lt;li&gt;Did retry volume spike before the breaker opened?&lt;/li&gt;
&lt;li&gt;Did the dependency recover?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot observe the breaker, you cannot tune it. For structured logging that pairs well with metrics, see &lt;a href="https://www.glukhov.org/observability/logging/structured-logging-go-slog/" rel="noopener noreferrer"&gt;Structured Logging in Go with slog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Production-Friendly HTTP Client Shape
&lt;/h2&gt;

&lt;p&gt;For real services, avoid scattering circuit breaker logic across handlers.&lt;/p&gt;

&lt;p&gt;Create a small client package around the dependency.&lt;/p&gt;

&lt;p&gt;Example structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal/
  userservice/
    client.go
    errors.go
    metrics.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler should not know the details of gobreaker. It should depend on a domain-level client method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;Then the implementation can contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP request creation&lt;/li&gt;
&lt;li&gt;context propagation&lt;/li&gt;
&lt;li&gt;breaker execution&lt;/li&gt;
&lt;li&gt;status code handling&lt;/li&gt;
&lt;li&gt;response decoding&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;error mapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the resilience policy close to the dependency boundary. For more on error classification at boundaries, see &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture: Boundaries and Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Circuit Breakers Fit in Application Architecture
&lt;/h2&gt;

&lt;p&gt;The circuit breaker pattern belongs at integration boundaries.&lt;/p&gt;

&lt;p&gt;In a Go application, that usually means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    A[Handler] --&amp;gt; B[Application Service]
    B --&amp;gt; C[Dependency Client]
    C --&amp;gt; D[Circuit Breaker]
    D --&amp;gt; E[HTTP / RPC / DB / Queue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the breaker out of business logic when possible.&lt;/p&gt;

&lt;p&gt;The business layer should understand domain errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment provider unavailable
recommendations unavailable
profile service timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should not need to understand gobreaker states.&lt;/p&gt;

&lt;p&gt;This separation keeps the architecture clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transport concerns stay in clients&lt;/li&gt;
&lt;li&gt;resilience policy stays near dependencies&lt;/li&gt;
&lt;li&gt;domain logic stays readable&lt;/li&gt;
&lt;li&gt;handlers stay thin&lt;/li&gt;
&lt;li&gt;tests are easier to write&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: No Timeout
&lt;/h3&gt;

&lt;p&gt;A circuit breaker does not magically stop slow calls unless the calls return.&lt;/p&gt;

&lt;p&gt;If the outbound operation can hang forever, the breaker may not see a failure quickly enough.&lt;/p&gt;

&lt;p&gt;Always use timeouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: One Global Breaker for Everything
&lt;/h3&gt;

&lt;p&gt;Do not use one breaker for all dependencies.&lt;/p&gt;

&lt;p&gt;A failing email provider should not open the circuit for your payment provider. A slow search endpoint should not block user profile calls.&lt;/p&gt;

&lt;p&gt;Use separate breakers for separate dependency operations when their failure modes differ.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Counting Caller Errors as Dependency Failures
&lt;/h3&gt;

&lt;p&gt;If your service sends bad input and receives &lt;code&gt;400 Bad Request&lt;/code&gt;, that is usually not a downstream outage.&lt;/p&gt;

&lt;p&gt;Do not train the breaker on your own bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Retrying Non-Idempotent Writes
&lt;/h3&gt;

&lt;p&gt;Retries are not free. They can duplicate writes, payments, messages, or side effects.&lt;/p&gt;

&lt;p&gt;Make writes idempotent before retrying them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Hiding Outages Behind Fallbacks
&lt;/h3&gt;

&lt;p&gt;Fallbacks should degrade gracefully, not falsify reality.&lt;/p&gt;

&lt;p&gt;If a dependency is down, your metrics and logs should make that obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 6: Tuning Without Production Data
&lt;/h3&gt;

&lt;p&gt;Thresholds copied from examples are only starting points.&lt;/p&gt;

&lt;p&gt;Tune based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request volume&lt;/li&gt;
&lt;li&gt;normal error rate&lt;/li&gt;
&lt;li&gt;dependency latency&lt;/li&gt;
&lt;li&gt;user impact&lt;/li&gt;
&lt;li&gt;recovery time&lt;/li&gt;
&lt;li&gt;fallback quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mistake 7: Using Circuit Breakers Instead of Capacity Management
&lt;/h3&gt;

&lt;p&gt;A circuit breaker is not a substitute for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;load shedding&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;queue limits&lt;/li&gt;
&lt;li&gt;autoscaling&lt;/li&gt;
&lt;li&gt;database tuning&lt;/li&gt;
&lt;li&gt;connection pool limits&lt;/li&gt;
&lt;li&gt;upstream quotas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is one part of a resilience strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Defaults
&lt;/h2&gt;

&lt;p&gt;For a typical Go service calling an internal HTTP dependency, a reasonable starting point might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP client timeout: 2 to 5 seconds
per-request context timeout: based on caller SLA
breaker failure rule: 5 consecutive failures or 50 percent failure after 20 requests
open timeout: 10 to 30 seconds
half-open requests: 1 to 5
retry count: 1 to 3 attempts
retry backoff: exponential with jitter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not universal values. They are safe-ish starting points.&lt;/p&gt;

&lt;p&gt;For user-facing APIs, keep total latency budgets tight. For background jobs, you may tolerate longer waits. For payment providers, be much more careful with retries and idempotency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breaker Checklist
&lt;/h2&gt;

&lt;p&gt;Before adding a circuit breaker, answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What dependency is being protected?&lt;/li&gt;
&lt;li&gt;What operation is being protected?&lt;/li&gt;
&lt;li&gt;What errors count as dependency failure?&lt;/li&gt;
&lt;li&gt;What errors should be ignored by the breaker?&lt;/li&gt;
&lt;li&gt;What timeout applies to each call?&lt;/li&gt;
&lt;li&gt;Are retries allowed?&lt;/li&gt;
&lt;li&gt;Are writes idempotent?&lt;/li&gt;
&lt;li&gt;What happens when the circuit is open?&lt;/li&gt;
&lt;li&gt;Is there a fallback?&lt;/li&gt;
&lt;li&gt;Is the fallback visible in metrics?&lt;/li&gt;
&lt;li&gt;Who gets alerted if the circuit keeps opening?&lt;/li&gt;
&lt;li&gt;How will the breaker be tuned after deployment?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot answer these, adding a breaker may create more confusion than resilience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Circuit Breakers in Go
&lt;/h2&gt;

&lt;p&gt;Test behavior, not the internal state machine of the library.&lt;/p&gt;

&lt;p&gt;Useful tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependency succeeds and response is returned&lt;/li&gt;
&lt;li&gt;dependency fails repeatedly and circuit opens&lt;/li&gt;
&lt;li&gt;open circuit returns a temporary error&lt;/li&gt;
&lt;li&gt;client-side validation errors do not trip the breaker&lt;/li&gt;
&lt;li&gt;context timeout is respected&lt;/li&gt;
&lt;li&gt;fallback response is returned when expected&lt;/li&gt;
&lt;li&gt;metrics are emitted on state changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use fake HTTP servers for integration-style tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusServiceUnavailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For unit tests, hide the dependency behind an interface and inject a fake implementation.&lt;/p&gt;

&lt;p&gt;Keep tests deterministic. Avoid sleeping for long real durations. Configure short breaker timeouts in tests. For more on testing concurrent Go code with fake time and isolated bubbles, see &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/testing-concurrent-go-code-synctest/" rel="noopener noreferrer"&gt;Testing Concurrent Go Code with testing/synctest&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Build Your Own Circuit Breaker?
&lt;/h2&gt;

&lt;p&gt;Building a small circuit breaker is a good learning exercise. It helps you understand the state machine.&lt;/p&gt;

&lt;p&gt;For production code, prefer a maintained library unless your needs are very specific.&lt;/p&gt;

&lt;p&gt;A production breaker needs to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;state transitions&lt;/li&gt;
&lt;li&gt;counters&lt;/li&gt;
&lt;li&gt;half-open probes&lt;/li&gt;
&lt;li&gt;callbacks&lt;/li&gt;
&lt;li&gt;custom failure classification&lt;/li&gt;
&lt;li&gt;race-free behavior&lt;/li&gt;
&lt;li&gt;predictable error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not impossible, but it is easy to get subtly wrong.&lt;/p&gt;

&lt;p&gt;The boring library is usually the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The circuit breaker pattern is not magic reliability dust.&lt;/p&gt;

&lt;p&gt;In Go, it works best when it is part of a small, explicit resilience stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context timeout
+ retry with backoff and jitter
+ circuit breaker
+ fallback
+ metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is most useful at dependency boundaries, especially around remote services that can become slow or partially unavailable.&lt;/p&gt;

&lt;p&gt;Use it to stop cascading failures. Use it to fail fast when a dependency is clearly unhealthy. Use it to give overloaded systems room to recover.&lt;/p&gt;

&lt;p&gt;But do not use it as an excuse to ignore timeouts, idempotency, observability, or clean architecture.&lt;/p&gt;

&lt;p&gt;A good circuit breaker makes failure clearer and cheaper. A bad one just makes failure more mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/go-microservices-for-ai-ml-orchestration-patterns/" rel="noopener noreferrer"&gt;Go Microservices for AI/ML Orchestration&lt;/a&gt; — broader orchestration context where circuit breakers fit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/saga-pattern-distributed-transactions/" rel="noopener noreferrer"&gt;Saga Pattern in Distributed Transactions&lt;/a&gt; — distributed transaction patterns that pair with circuit breakers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems&lt;/a&gt; — retry safety and idempotent operations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/transactional-outbox-pattern-go/" rel="noopener noreferrer"&gt;Transactional Outbox Pattern in Go&lt;/a&gt; — reliable event delivery alongside resilience patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture&lt;/a&gt; — error classification at dependency boundaries&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/testing-concurrent-go-code-synctest/" rel="noopener noreferrer"&gt;Testing Concurrent Go Code with synctest&lt;/a&gt; — testing async behavior with circuit breakers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/observability/logging/structured-logging-go-slog/" rel="noopener noreferrer"&gt;Structured Logging in Go with slog&lt;/a&gt; — observability alongside circuit breakers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;github.com/sony/gobreaker/v2&lt;/code&gt; — official gobreaker v2 package&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-context-cancellation-timeouts/" rel="noopener noreferrer"&gt;Go Context Cancellation and Timeouts&lt;/a&gt; — context patterns that pair with circuit breakers&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>dev</category>
    </item>
    <item>
      <title>Podman Quadlet vs Docker Compose for Linux Services</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:13:30 +0000</pubDate>
      <link>https://dev.to/rosgluk/podman-quadlet-vs-docker-compose-for-linux-services-gci</link>
      <guid>https://dev.to/rosgluk/podman-quadlet-vs-docker-compose-for-linux-services-gci</guid>
      <description>&lt;p&gt;Docker Compose and Podman Quadlet solve overlapping problems but come from different design centers, and choosing between them depends on whether you think in application stacks or Linux services.&lt;/p&gt;

&lt;p&gt;The distinction matters for anyone running containers on a Linux host beyond a single afternoon of experimentation. Compose describes services, networks, and volumes in a YAML file and starts them with &lt;code&gt;docker compose up&lt;/code&gt;. Quadlet describes containers in systemd-style unit files and lets the system service manager own the lifecycle.&lt;/p&gt;

&lt;p&gt;Both approaches work for self-hosted services, internal tools, and small servers. The right choice comes down to your operational model, team familiarity, and whether you prefer a developer-friendly stack format or a systemd-native service model. This comparison covers the practical differences: file formats, lifecycle ownership, rootless containers, logging, updates, networking, security, and migration paths. It is part of &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Recommendation
&lt;/h2&gt;

&lt;p&gt;Use Docker Compose when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the fastest multi-container workflow.&lt;/li&gt;
&lt;li&gt;You already use &lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Docker Engine&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;You share stacks with developers.&lt;/li&gt;
&lt;li&gt;You need a familiar &lt;code&gt;compose.yaml&lt;/code&gt; for local development.&lt;/li&gt;
&lt;li&gt;You deploy small services with Docker on one host.&lt;/li&gt;
&lt;li&gt;You use existing Compose examples from projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Podman Quadlet when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want systemd-native container services.&lt;/li&gt;
&lt;li&gt;You prefer rootless containers.&lt;/li&gt;
&lt;li&gt;You do not want a central Docker daemon.&lt;/li&gt;
&lt;li&gt;You run long-lived services on a Linux host.&lt;/li&gt;
&lt;li&gt;You want &lt;code&gt;systemctl&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, timers, dependencies, and auto-start.&lt;/li&gt;
&lt;li&gt;You are building a self-hosted or homelab server around systemd.&lt;/li&gt;
&lt;li&gt;You want containers to fit into the normal Linux service model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Compose is better for application stacks.
Podman Quadlet is better for Linux services.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a law. It is a useful default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary model&lt;/td&gt;
&lt;td&gt;Multi-container application&lt;/td&gt;
&lt;td&gt;systemd-managed container service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File format&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;systemd-like unit files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Docker Engine&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daemon&lt;/td&gt;
&lt;td&gt;Uses Docker daemon&lt;/td&gt;
&lt;td&gt;Daemonless Podman model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service manager&lt;/td&gt;
&lt;td&gt;Compose manages stack lifecycle&lt;/td&gt;
&lt;td&gt;systemd manages lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Dev stacks, app bundles, simple deployments&lt;/td&gt;
&lt;td&gt;Long-running Linux services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rootless support&lt;/td&gt;
&lt;td&gt;Possible, but not the default mental model&lt;/td&gt;
&lt;td&gt;Strong fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;journalctl&lt;/code&gt; and &lt;code&gt;podman logs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup on boot&lt;/td&gt;
&lt;td&gt;Usually via systemd wrapper or restart policy&lt;/td&gt;
&lt;td&gt;Native systemd unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman auto-update or systemd workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability&lt;/td&gt;
&lt;td&gt;Very high across Docker environments&lt;/td&gt;
&lt;td&gt;Best on Linux with systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Easier for most developers&lt;/td&gt;
&lt;td&gt;Easier for systemd users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem examples&lt;/td&gt;
&lt;td&gt;Huge&lt;/td&gt;
&lt;td&gt;Smaller, but growing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither one is Kubernetes. Most small services do not need a cluster. They need a boring, understandable way to start, stop, update, log, and recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Docker Compose Is Good At
&lt;/h2&gt;

&lt;p&gt;Docker Compose is a tool for defining and running multi-container applications. A typical Compose file describes services, images, build contexts, ports, volumes, networks, environment variables, health checks, dependencies, and profiles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:stable&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/usr/share/nginx/html:ro&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose is direct and productive. It is especially good when the unit of thought is "this application has several containers." For a comprehensive reference of Compose commands and patterns, see the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt;. For Docker commands beyond Compose — images, volumes, networks, and cleanup — see the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Podman Quadlet Is Good At
&lt;/h2&gt;

&lt;p&gt;Podman Quadlet is a way to define Podman containers using systemd-style files. Instead of writing a full generated systemd service by hand, you write a declarative file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example web container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:stable&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:80&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/example/html:/usr/share/nginx/html:ro&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;/etc/containers/systemd/example.container&lt;/code&gt;, then reload systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; example.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status example.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; example.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core appeal of Quadlet: the container becomes a normal Linux service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Philosophical Difference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Thinks in Stacks
&lt;/h3&gt;

&lt;p&gt;Compose asks: &lt;em&gt;What services make up this application?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Compose project usually lives near application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myapp/
  compose.yaml
  .env
  app/
  db/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You start the project as a unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You update the project as a unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple, visible, and portable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Podman Quadlet Thinks in Services
&lt;/h3&gt;

&lt;p&gt;Quadlet asks: &lt;em&gt;What containers should this Linux host run as services?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Quadlet files live in systemd-related container paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/containers/systemd/
~/.config/containers/systemd/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You manage generated services with systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myapp.service
systemctl restart myapp.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels more native on a Linux server. For general systemd service patterns, see &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/executable-as-a-service-in-linux/" rel="noopener noreferrer"&gt;Run any Executable as a Service in Linux&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Important Difference: Who Owns Lifecycle?
&lt;/h2&gt;

&lt;p&gt;With Docker Compose, Compose owns the application lifecycle. With Quadlet, systemd owns the service lifecycle.&lt;/p&gt;

&lt;p&gt;This affects boot behavior, shutdown behavior, restart policy, dependency ordering, logs, health visibility, user services, updates, integration with timers, and integration with other host services.&lt;/p&gt;

&lt;p&gt;If you already use systemd to manage everything else on the host, Quadlet fits neatly. If you think mainly in terms of application stacks, Compose is usually more comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Compose Under systemd vs Quadlet
&lt;/h2&gt;

&lt;p&gt;You can run Docker Compose as a systemd service. That is often a good pattern. Example systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well. But it is still a wrapper around Compose. systemd starts the Compose command, while Docker and Compose handle containers behind it.&lt;/p&gt;

&lt;p&gt;With Quadlet, the unit generation is designed for Podman and systemd directly. You write container-oriented unit files, and systemd manages the generated services.&lt;/p&gt;

&lt;p&gt;The distinction is subtle but important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Compose under systemd:
  systemd manages a Compose command.

Podman Quadlet:
  systemd manages generated container services.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a detailed walkthrough of this pattern, see &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-as-systemd-service/" rel="noopener noreferrer"&gt;Run Docker Compose as a Linux Service with systemd&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Format Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose YAML
&lt;/h3&gt;

&lt;p&gt;Compose uses YAML. It is compact, popular, and easy to share. It is also indentation-sensitive and can grow messy when a stack becomes large.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/example/app:1.0.0&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;APP_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-data:/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quadlet Unit Files
&lt;/h3&gt;

&lt;p&gt;Quadlet uses systemd-like files. They are more verbose when you have many services, but readable if you already understand systemd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example app container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ghcr.io/example/app:1.0.0&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:8080&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;APP_ENV=production&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data.volume:/data&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a volume file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Volume]&lt;/span&gt;
&lt;span class="py"&gt;VolumeName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saved as &lt;code&gt;app.container&lt;/code&gt; and &lt;code&gt;app-data.volume&lt;/code&gt; in the appropriate systemd container directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Docker Compose Concepts to Quadlet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compose concept&lt;/th&gt;
&lt;th&gt;Quadlet equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.container&lt;/code&gt; files or &lt;code&gt;.pod&lt;/code&gt; plus &lt;code&gt;.container&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.volume&lt;/code&gt; files or bind mounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;networks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.network&lt;/code&gt; files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PublishPort=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Environment=&lt;/code&gt; or &lt;code&gt;EnvironmentFile=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[Service] Restart=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;depends_on&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;systemd &lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Wants=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;healthcheck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman healthcheck options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;profiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;systemd enablement and separate units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;journalctl -u service&lt;/code&gt; and &lt;code&gt;podman logs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl stop service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;project directory&lt;/td&gt;
&lt;td&gt;systemd container unit directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The migration is conceptually simple but not mechanical. Compose describes a stack. Quadlet describes services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rootless Containers
&lt;/h2&gt;

&lt;p&gt;Rootless containers are one of the strongest reasons to look at Podman and Quadlet.&lt;/p&gt;

&lt;p&gt;With Docker, many users add themselves to the &lt;code&gt;docker&lt;/code&gt; group. That is convenient, but access to the Docker daemon is effectively powerful host access. On a personal workstation, that may be acceptable. On shared servers, it deserves more caution.&lt;/p&gt;

&lt;p&gt;Podman was designed with rootless usage as a first-class workflow. A rootless Quadlet lives under the user's config directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/containers/systemd/whoami.container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then manage it with user systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status whoami.service
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; whoami.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To allow the user service to keep running after logout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a clean model for user-owned services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rootless Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default common setup&lt;/td&gt;
&lt;td&gt;Rootful Docker daemon&lt;/td&gt;
&lt;td&gt;Rootless-friendly Podman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User service model&lt;/td&gt;
&lt;td&gt;Possible, but less native&lt;/td&gt;
&lt;td&gt;Native with &lt;code&gt;systemctl --user&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daemon access risk&lt;/td&gt;
&lt;td&gt;Docker socket is powerful&lt;/td&gt;
&lt;td&gt;No central root daemon by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low port binding&lt;/td&gt;
&lt;td&gt;Simple as rootful Docker&lt;/td&gt;
&lt;td&gt;Needs extra setup when rootless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host integration&lt;/td&gt;
&lt;td&gt;Very common&lt;/td&gt;
&lt;td&gt;More Linux-native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared server fit&lt;/td&gt;
&lt;td&gt;Needs care&lt;/td&gt;
&lt;td&gt;Strong fit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rootless is not magic. It has tradeoffs around networking, privileged behavior, and low ports. But for long-running user-owned services, Quadlet is a very elegant model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup and Boot Behavior
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose
&lt;/h3&gt;

&lt;p&gt;Compose by itself does not create a boot service. You usually rely on Docker restart policies, a systemd wrapper around &lt;code&gt;docker compose up -d&lt;/code&gt;, a deployment script, or a higher-level tool.&lt;/p&gt;

&lt;p&gt;Example Compose restart policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A systemd wrapper gives you a host-level service. That is good, but it is still an extra wrapper. See &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-as-systemd-service/" rel="noopener noreferrer"&gt;Run Docker Compose as a Linux Service with systemd&lt;/a&gt; for the full pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Podman Quadlet
&lt;/h3&gt;

&lt;p&gt;Quadlet is already systemd-oriented. Enable on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boot behavior is not an add-on. It is the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restart Behavior
&lt;/h2&gt;

&lt;p&gt;Compose commonly uses &lt;code&gt;restart: unless-stopped&lt;/code&gt; in YAML. Quadlet commonly uses systemd restart behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moves restart logic into the service manager. The preference: use Compose/Docker restart policies for Compose stacks, use systemd restart policies for Quadlet, and do not stack too many supervisors. Keep one clear owner of restart behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is excellent for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quadlet Logs
&lt;/h3&gt;

&lt;p&gt;Quadlet services use systemd logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless units:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can still use Podman logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman logs &lt;span class="nt"&gt;-f&lt;/span&gt; container-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For server operations, &lt;code&gt;journalctl&lt;/code&gt; integration is a major advantage. Your containers fit into the same log workflow as other Linux services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Updating Docker Compose
&lt;/h3&gt;

&lt;p&gt;A common update flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy to wrap in a script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp

docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt;
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating Podman Quadlet
&lt;/h3&gt;

&lt;p&gt;A simplified manual flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;podman pull ghcr.io/example/app:1.0.1
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for rootless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman pull ghcr.io/example/app:1.0.1
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; restart app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podman can support auto-update workflows when containers are configured with the right labels and image policy. Quadlet's advantage is not that updates are always simpler. The advantage is that updates are service-manager-native.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-Update Philosophy
&lt;/h2&gt;

&lt;p&gt;Auto-updates are convenient. They are also a risk. For low-risk homelab services, automatic container updates can be fine. For databases, stateful apps, or business services, the preferred flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back up.&lt;/li&gt;
&lt;li&gt;Pull.&lt;/li&gt;
&lt;li&gt;Recreate or restart.&lt;/li&gt;
&lt;li&gt;Check health.&lt;/li&gt;
&lt;li&gt;Prune later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compose makes this explicit. Quadlet and Podman can make it systemd-native. Neither tool removes the need for a rollback plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Volumes and Persistent Data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compose Volumes
&lt;/h3&gt;

&lt;p&gt;Compose supports named volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And bind mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/config:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quadlet Volumes
&lt;/h3&gt;

&lt;p&gt;Quadlet can use bind mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/config:/config:ro&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/data:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or a &lt;code&gt;.volume&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Volume]&lt;/span&gt;
&lt;span class="py"&gt;VolumeName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data.volume:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose is more compact for stack-level storage. Quadlet is more aligned with independently managed service units.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets and Environment Files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compose
&lt;/h3&gt;

&lt;p&gt;Compose often uses &lt;code&gt;env_file&lt;/code&gt; or &lt;code&gt;environment&lt;/code&gt; in YAML. For a small private service, &lt;code&gt;.env&lt;/code&gt; is common. For serious systems, treat &lt;code&gt;.env&lt;/code&gt; as sensitive and keep it out of Git.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quadlet
&lt;/h3&gt;

&lt;p&gt;Quadlet can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;APP_ENV=production&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/app.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restrict permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /opt/app/app.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither Compose nor Quadlet is a complete secret-management system by itself. Do not confuse "not in the command line" with "secure".&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compose Networking
&lt;/h3&gt;

&lt;p&gt;Compose creates a default project network and gives services DNS names based on service names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app can usually reach the database at &lt;code&gt;db&lt;/code&gt;. Multi-container app networking feels natural. This is one of Compose's strongest features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quadlet Networking
&lt;/h3&gt;

&lt;p&gt;Quadlet can define networks separately with &lt;code&gt;.network&lt;/code&gt; files or use Podman network options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Network]&lt;/span&gt;
&lt;span class="py"&gt;NetworkName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;appnet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;example/app:stable&lt;/span&gt;
&lt;span class="py"&gt;Network&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;appnet.network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more explicit and systemd-like. For one or two containers, it is fine. For a large app stack, Compose is often easier to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pods
&lt;/h2&gt;

&lt;p&gt;Podman has a native pod concept. That matters if you like the Kubernetes mental model where multiple containers share a network namespace and lifecycle boundary. Quadlet supports &lt;code&gt;.pod&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Pod]&lt;/span&gt;
&lt;span class="py"&gt;PodName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A container can join that pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ghcr.io/example/app:stable&lt;/span&gt;
&lt;span class="py"&gt;Pod&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp.pod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose does not have the same pod model. It has services on networks. For most simple web apps, Compose networks are enough. For Podman users who like pod-style grouping, Quadlet is a better match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Workflows
&lt;/h2&gt;

&lt;p&gt;Compose is usually better when you build images as part of the local application workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is extremely convenient for development. Quadlet is usually better when you run already-built images as services. Build images with Podman separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman build &lt;span class="nt"&gt;-t&lt;/span&gt; localhost/myapp:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;localhost/myapp:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your workflow is "edit code, rebuild, restart stack", Compose wins. If your workflow is "deploy a known image as a Linux service", Quadlet wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability
&lt;/h2&gt;

&lt;p&gt;Compose files are widely shared. Many open-source projects provide a &lt;code&gt;compose.yaml&lt;/code&gt; or &lt;code&gt;docker-compose.yml&lt;/code&gt;. If a project says "run this with Docker Compose", you can usually start quickly with &lt;code&gt;docker compose up -d&lt;/code&gt;. This is a major practical advantage.&lt;/p&gt;

&lt;p&gt;Quadlet is portable across systems that have Podman, systemd, and compatible Quadlet support. That is a narrower target, but a very good one for modern Linux servers. Quadlet is not the best format for sharing an application with every possible developer. It is a good format for describing how a specific Linux host should run a containerized service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Experience
&lt;/h2&gt;

&lt;p&gt;Docker Compose usually wins developer experience. More examples, more tutorials, more project templates, easier local builds, easy one-file stack, familiar &lt;code&gt;docker compose up&lt;/code&gt;, and strong fit for dev dependencies. A developer can read this quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quadlet can do similar things, but it is more operations-shaped. For local development, I would rarely start with Quadlet unless the application itself is specifically about Podman or systemd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations Experience
&lt;/h2&gt;

&lt;p&gt;Quadlet often wins operations experience on a Linux host. Reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native &lt;code&gt;systemctl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Native &lt;code&gt;journalctl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rootless user services&lt;/li&gt;
&lt;li&gt;systemd dependencies&lt;/li&gt;
&lt;li&gt;systemd timers&lt;/li&gt;
&lt;li&gt;systemd restart behavior&lt;/li&gt;
&lt;li&gt;No central Docker daemon&lt;/li&gt;
&lt;li&gt;Better fit with host service management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A server admin can reason about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status app.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
systemctl restart app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the normal Linux service workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Security Notes
&lt;/h3&gt;

&lt;p&gt;Docker Compose usually talks to the Docker daemon. On a normal Linux Docker install, access to the Docker socket is powerful. A user who can control Docker can often mount host paths, run privileged containers, or otherwise gain broad host control. For installation options including rootless Docker on Ubuntu, see &lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Practical advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not casually expose &lt;code&gt;/var/run/docker.sock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Treat the &lt;code&gt;docker&lt;/code&gt; group as privileged.&lt;/li&gt;
&lt;li&gt;Avoid privileged containers.&lt;/li&gt;
&lt;li&gt;Avoid host mounts unless needed.&lt;/li&gt;
&lt;li&gt;Keep secrets out of Git.&lt;/li&gt;
&lt;li&gt;Use explicit image tags for important services.&lt;/li&gt;
&lt;li&gt;Review published ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quadlet Security Notes
&lt;/h3&gt;

&lt;p&gt;Podman Quadlet pairs well with rootless containers and user systemd services. This can reduce risk, especially on shared hosts or personal servers where services should not require a root daemon.&lt;/p&gt;

&lt;p&gt;Practical advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer rootless services when they fit.&lt;/li&gt;
&lt;li&gt;Use user units for user-owned services.&lt;/li&gt;
&lt;li&gt;Use system units only when host-level privileges are needed.&lt;/li&gt;
&lt;li&gt;Avoid unnecessary privileged containers.&lt;/li&gt;
&lt;li&gt;Keep environment files locked down.&lt;/li&gt;
&lt;li&gt;Think carefully about bind mounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rootless does not mean risk-free. It means the default blast radius can be smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;For most web services, internal tools, and self-hosted apps, performance is not the deciding factor. The main differences are operational, not raw speed. Choose based on lifecycle model, security model, host integration, team familiarity, update process, networking needs, and debugging workflow.&lt;/p&gt;

&lt;p&gt;If you are choosing between Compose and Quadlet because of performance alone, you are probably optimizing the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Failure Modes
&lt;/h3&gt;

&lt;p&gt;Common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker daemon not running&lt;/li&gt;
&lt;li&gt;Compose plugin missing&lt;/li&gt;
&lt;li&gt;Wrong project directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; not loaded as expected&lt;/li&gt;
&lt;li&gt;Old &lt;code&gt;docker-compose&lt;/code&gt; binary used by accident&lt;/li&gt;
&lt;li&gt;Containers not recreated after config changes&lt;/li&gt;
&lt;li&gt;Orphan containers left after service rename&lt;/li&gt;
&lt;li&gt;Volumes deleted with &lt;code&gt;down -v&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docker logs filling the disk&lt;/li&gt;
&lt;li&gt;Docker socket permission errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best fixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose config
docker compose ps
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Podman Quadlet Failure Modes
&lt;/h3&gt;

&lt;p&gt;Common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit file in the wrong directory&lt;/li&gt;
&lt;li&gt;Forgot &lt;code&gt;systemctl daemon-reload&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Using system units when user units were intended&lt;/li&gt;
&lt;li&gt;Forgot &lt;code&gt;loginctl enable-linger&lt;/code&gt; for rootless services&lt;/li&gt;
&lt;li&gt;Image pull takes longer than systemd startup timeout&lt;/li&gt;
&lt;li&gt;cgroup v2 not available&lt;/li&gt;
&lt;li&gt;SELinux labels or volume permissions&lt;/li&gt;
&lt;li&gt;Service name differs from file expectations&lt;/li&gt;
&lt;li&gt;Network or volume unit not enabled or referenced correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best fixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status app.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
systemctl daemon-reload
podman ps &lt;span class="nt"&gt;-a&lt;/span&gt;
podman logs container-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status app.service
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migration Example: Compose to Quadlet
&lt;/h2&gt;

&lt;p&gt;Start with this Compose service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whoami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik/whoami:v1.10&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WHOAMI_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose-demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rough Quadlet equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Whoami demo container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik/whoami:v1.10&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:80&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;WHOAMI_NAME=quadlet-demo&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save as &lt;code&gt;/etc/containers/systemd/whoami.container&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example is easy because it is one container. A larger Compose stack with databases, networks, volumes, and build steps needs more careful translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Before moving from Compose to Quadlet, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Is this stack really a set of long-running host services?
[ ] Are the images already built and published?
[ ] Do I need rootless services?
[ ] Do I want systemd dependencies and timers?
[ ] Are volumes and bind mounts clearly understood?
[ ] Are ports documented?
[ ] Are secrets handled outside Git?
[ ] Is there a backup and restore process?
[ ] Can I monitor logs through journalctl?
[ ] Do I have a rollback path?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If most answers are yes, Quadlet may be a good fit. If the stack is mostly for local development, Compose is probably still better.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Stay with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Stay with Compose when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project already ships a good Compose file.&lt;/li&gt;
&lt;li&gt;You need the easiest onboarding path.&lt;/li&gt;
&lt;li&gt;Developers run the same stack locally.&lt;/li&gt;
&lt;li&gt;You build images during development.&lt;/li&gt;
&lt;li&gt;You want one YAML file for services, volumes, and networks.&lt;/li&gt;
&lt;li&gt;You want maximum tutorial and community compatibility.&lt;/li&gt;
&lt;li&gt;Your current systemd wrapper works fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no prize for migrating a working Compose stack to Quadlet just because Quadlet is cleaner in theory. If Compose is boring and reliable for your use case, keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Move to Podman Quadlet
&lt;/h2&gt;

&lt;p&gt;Move to Quadlet when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The stack is really a host service.&lt;/li&gt;
&lt;li&gt;You want rootless service management.&lt;/li&gt;
&lt;li&gt;You prefer Podman over Docker.&lt;/li&gt;
&lt;li&gt;You want systemd to own lifecycle.&lt;/li&gt;
&lt;li&gt;You want &lt;code&gt;journalctl&lt;/code&gt; logs.&lt;/li&gt;
&lt;li&gt;You want service dependencies.&lt;/li&gt;
&lt;li&gt;You want user services that survive logout.&lt;/li&gt;
&lt;li&gt;You want less Docker daemon exposure.&lt;/li&gt;
&lt;li&gt;You are building a self-hosting host around systemd.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quadlet is not "Compose but better." It is a different design center.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Local Development
&lt;/h3&gt;

&lt;p&gt;Use Docker Compose. Fast, familiar, portable, easy to rebuild, easy for teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Single-Host Self-Hosting
&lt;/h3&gt;

&lt;p&gt;Use either. Choose Compose if the project already provides a Compose file. Choose Quadlet if you want systemd-native service management. Compose gives a better app bundle; Quadlet gives a better Linux service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: User-Owned Rootless Service
&lt;/h3&gt;

&lt;p&gt;Use Podman Quadlet. Rootless workflow, user-level service management, no central Docker daemon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/containers/systemd/app.container
systemctl --user enable --now app.service
loginctl enable-linger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 4: Production-Like Single Server
&lt;/h3&gt;

&lt;p&gt;Use Docker Compose with a disciplined systemd wrapper, or use Quadlet if your team is comfortable with Podman. Do not choose based on fashion. Choose based on who will operate it at 2 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 5: Multi-Node Platform
&lt;/h3&gt;

&lt;p&gt;Use neither as the final orchestration layer. Consider Kubernetes, Nomad, Swarm, or a managed platform. Compose and Quadlet are excellent single-host tools. They are not cluster schedulers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Decision Tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this mainly for local development?
  yes:
    use Docker Compose
  no:
    continue

Does the project already provide a maintained compose.yaml?
  yes:
    use Docker Compose unless you have a strong reason to migrate
  no:
    continue

Do you want rootless long-running services managed by systemd?
  yes:
    use Podman Quadlet
  no:
    continue

Do you want the easiest multi-container app definition?
  yes:
    use Docker Compose
  no:
    continue

Do you want containers to behave like normal Linux services?
  yes:
    use Podman Quadlet
  no:
    use Docker Compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Side-by-Side Commands
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl stop app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl restart app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apply changes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl daemon-reload &amp;amp;&amp;amp; systemctl restart app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs -f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl -u app.service -f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl status app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable on boot&lt;/td&gt;
&lt;td&gt;systemd wrapper or restart policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl enable app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose pull&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;podman pull image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rootless service&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;natural with &lt;code&gt;systemctl --user&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common Misunderstandings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Quadlet Replaces Docker Compose"
&lt;/h3&gt;

&lt;p&gt;Not exactly. Quadlet replaces some Compose use cases, especially long-running Linux services. It does not replace Compose as the easiest application-stack format for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Docker Compose Is Not Production Ready"
&lt;/h3&gt;

&lt;p&gt;Too broad. Compose can be perfectly reasonable for small production systems if you understand backups, updates, logging, restart behavior, and host security. The problem is not Compose. The problem is pretending a single-host Compose deployment has the same properties as a cluster orchestrator.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Podman Is Just Docker Without the Daemon"
&lt;/h3&gt;

&lt;p&gt;Too simple. Podman has Docker-compatible commands, but its design center is different: daemonless operation, rootless workflows, pods, and Linux integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Rootless Means Secure"
&lt;/h3&gt;

&lt;p&gt;No. Rootless reduces some risks. It does not make bad images, exposed secrets, unsafe bind mounts, or vulnerable apps safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  "systemd Is Too Heavy for Containers"
&lt;/h3&gt;

&lt;p&gt;systemd is already the service manager on most mainstream Linux servers. Using it to manage long-running containers is not strange. It is often the boring and correct thing to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendation
&lt;/h2&gt;

&lt;p&gt;Use Docker Compose when the application stack is the main thing. Use Podman Quadlet when the Linux service is the main thing.&lt;/p&gt;

&lt;p&gt;That distinction is more useful than arguing which tool is better. For developer workflows, Compose is hard to beat. It is popular, readable, portable, and supported by countless projects. For long-running Linux services, Quadlet is quietly excellent. It makes containers feel like native systemd services, works naturally with rootless Podman, and fits the operational model of a serious Linux host.&lt;/p&gt;

&lt;p&gt;The preferred split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local development: Docker Compose
Portable app examples: Docker Compose
Small self-hosted stacks: Docker Compose or Quadlet
Rootless user services: Podman Quadlet
Long-running host services: Podman Quadlet
Multi-node orchestration: neither; use a real orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not migrate just to be modern. Migrate when the lifecycle model is better. Compose is a great stack tool. Quadlet is a great service tool. The smart choice is to use each where its mental model matches the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html" rel="noopener noreferrer"&gt;Podman Quadlet Documentation&lt;/a&gt; — official Podman Quadlet reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose Documentation&lt;/a&gt; — official Docker Compose reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-as-systemd-service/" rel="noopener noreferrer"&gt;Run Docker Compose as a Linux Service with systemd&lt;/a&gt; — detailed Compose-as-systemd walkthrough on this site&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt; — Compose commands and patterns reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt; — Docker commands reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu&lt;/a&gt; — Docker installation guide with rootless alternatives&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/executable-as-a-service-in-linux/" rel="noopener noreferrer"&gt;Run any Executable as a Service in Linux&lt;/a&gt; — general systemd service patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt; — cluster home&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>devops</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Hermes Agent: Headless Server + Remote Desktop Setup</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:07:27 +0000</pubDate>
      <link>https://dev.to/rosgluk/hermes-agent-headless-server-remote-desktop-setup-3dhd</link>
      <guid>https://dev.to/rosgluk/hermes-agent-headless-server-remote-desktop-setup-3dhd</guid>
      <description>&lt;p&gt;Running Hermes Agent on a headless server while connecting from a desktop client on another machine requires two server processes and a single client connection.&lt;/p&gt;

&lt;p&gt;The architecture separates the Hermes backend into two server-side processes and one client-side surface. The &lt;code&gt;hermes serve&lt;/code&gt; backend handles the API and dashboard connections, while the &lt;code&gt;hermes gateway run&lt;/code&gt; process manages messaging channels independently. The desktop client connects to the serve backend, not the gateway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph Server["HEADLESS SERVER"]
        serve["hermes serve&amp;lt;br/&amp;gt;--host 0.0.0.0&amp;lt;br/&amp;gt;:9119"]
        gateway["hermes gateway run&amp;lt;br/&amp;gt;Telegram, Discord, Slack"]
    end

    subgraph Client["DESKTOP PC"]
        desktop["hermes desktop&amp;lt;br/&amp;gt;(WebSocket connection)"]
    end

    desktop &amp;lt;---&amp;gt;|WebSocket| serve
    gateway -.-&amp;gt;|Shares ~/.hermes/| serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two processes on the server, one app on the client. Both server processes share the same &lt;code&gt;~/.hermes/&lt;/code&gt; config, skills, memory, and sessions. Cron jobs execute on the server where the gateway runs.&lt;/p&gt;

&lt;p&gt;For installation, provider setup, and initial configuration, start with the &lt;a href="https://www.glukhov.org/ai-systems/hermes/" rel="noopener noreferrer"&gt;Hermes AI Assistant — Install, Setup, Workflow, and Troubleshooting&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the headless server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Configure authentication
&lt;/h3&gt;

&lt;p&gt;Basic auth provides sufficient protection for a trusted LAN. Add credentials to the environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.hermes/.env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=choose-a-strong-password
HERMES_DASHBOARD_BASIC_AUTH_SECRET=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.hermes/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file lives alongside &lt;code&gt;config.yaml&lt;/code&gt; under &lt;code&gt;~/.hermes/&lt;/code&gt;. Hermes resolves configuration with CLI overrides first, then &lt;code&gt;config.yaml&lt;/code&gt;, then &lt;code&gt;.env&lt;/code&gt;, then built-in defaults. Secrets belong in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start the backend
&lt;/h3&gt;

&lt;p&gt;Run the serve backend on all interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes serve &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 9119
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend listens on port 9119 and accepts WebSocket connections from the desktop client. Verify it is running with a quick status check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:9119/api/status | jq &lt;span class="s1"&gt;'.auth_required, .auth_providers'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: true  "basic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Run as a systemd service
&lt;/h3&gt;

&lt;p&gt;For a persistent server that survives reboots, install a user-level systemd service. Create the unit file at &lt;code&gt;/etc/systemd/user/hermes-serve.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Hermes Agent Serve Backend&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%h/.hermes/.env&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/rg/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main serve --host 0.0.0.0 --port 9119&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the daemon, enable the service, and start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;hermes-serve
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start hermes-serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the service status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status hermes-serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Start the gateway
&lt;/h3&gt;

&lt;p&gt;The gateway is a separate process that handles messaging channels — Telegram, Discord, Slack, and others. Start it independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes gateway run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway and the serve backend are two separate processes. The gateway manages sessions, runs cron jobs, and routes messages. The serve backend provides the API surface for desktop and web dashboard connections. They share the same home directory but run independently.&lt;/p&gt;

&lt;p&gt;For the full list of gateway commands and subcommands, see the &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-cli-cheatsheet/" rel="noopener noreferrer"&gt;Hermes Agent CLI cheat sheet&lt;/a&gt;.&lt;br&gt;
If your primary interface is mobile messaging, pair this setup with &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-voice-control/" rel="noopener noreferrer"&gt;Hermes Voice Control from Your Phone&lt;/a&gt; for voice-first workflows on top of the same gateway process.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Verify the backend
&lt;/h3&gt;

&lt;p&gt;Confirm the backend is responding and authentication is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:9119/api/status | jq &lt;span class="s1"&gt;'.auth_required, .auth_providers'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;true
"basic"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If authentication is not enabled, the response will show &lt;code&gt;false&lt;/code&gt; for &lt;code&gt;auth_required&lt;/code&gt;. Check that the &lt;code&gt;.env&lt;/code&gt; file contains the correct variables and that the service has restarted after configuration changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect from the desktop client
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: Hermes Desktop App
&lt;/h3&gt;

&lt;p&gt;Install Hermes Desktop from the &lt;a href="https://hermes-agent.nousresearch.com/" rel="noopener noreferrer"&gt;official site&lt;/a&gt;. Launch the app, navigate to &lt;strong&gt;Settings → Gateway → Remote gateway&lt;/strong&gt;, and enter the server address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;server-ip&amp;gt;:9119
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign in with the username and password you configured on the server.&lt;/p&gt;

&lt;p&gt;Alternatively, set the remote URL via environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HERMES_DESKTOP_REMOTE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;server-ip&amp;gt;:9119 hermes desktop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: Web dashboard
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;http://&amp;lt;server-ip&amp;gt;:9119&lt;/code&gt; in a browser and sign in with the basic auth credentials. The web dashboard provides a browser-based interface to the Hermes backend without requiring a desktop installation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: CLI
&lt;/h3&gt;

&lt;p&gt;From the desktop PC's terminal, configure the remote URL via the desktop app settings or the &lt;code&gt;HERMES_DESKTOP_REMOTE_URL&lt;/code&gt; environment variable. The CLI surface connects through the same WebSocket channel as the desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network and security considerations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trusted LAN&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--host 0.0.0.0&lt;/code&gt; + basic auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposed to internet&lt;/td&gt;
&lt;td&gt;Use Tailscale (&lt;code&gt;--host &amp;lt;tailscale-ip&amp;gt;&lt;/code&gt;) or OAuth provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall&lt;/td&gt;
&lt;td&gt;Open port 9119 TCP on the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a trusted local network, basic auth on &lt;code&gt;0.0.0.0&lt;/code&gt; is adequate. If the server is exposed to the internet, use Tailscale to bind to the Tailscale IP instead of &lt;code&gt;0.0.0.0&lt;/code&gt;, or configure an OAuth provider for stronger authentication. Always open port 9119 TCP in the server's firewall when the backend needs to accept external connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important process distinctions
&lt;/h2&gt;

&lt;p&gt;Understanding the separation between the two server processes prevents common configuration mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hermes serve&lt;/code&gt;&lt;/strong&gt; — The backend that the desktop app and web dashboard connect to. Handles the API surface and WebSocket connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hermes gateway run&lt;/code&gt;&lt;/strong&gt; — The process that handles Telegram, Discord, Slack, and other messaging channels. Manages sessions, runs cron jobs, and routes messages.&lt;/li&gt;
&lt;li&gt;These are &lt;strong&gt;two separate processes&lt;/strong&gt; on the server. They share &lt;code&gt;~/.hermes/&lt;/code&gt; config, skills, memory, and sessions, but run independently.&lt;/li&gt;
&lt;li&gt;Cron jobs execute on the server where the gateway runs.&lt;/li&gt;
&lt;li&gt;Profiles, skills, and memory are configured on the server side. The client connects to the already-configured backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For profile-first configuration and skills tuned to different production roles, see &lt;a href="https://www.glukhov.org/ai-systems/hermes/production-setup/" rel="noopener noreferrer"&gt;Hermes AI Assistant Skills for Real Production Setups&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Desktop cannot connect to the server
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Verify the backend is running: &lt;code&gt;systemctl --user status hermes-serve&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check the port is open: &lt;code&gt;ss -tlnp | grep 9119&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test from the server: &lt;code&gt;curl -s http://localhost:9119/api/status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test from the client machine: &lt;code&gt;curl -s http://&amp;lt;server-ip&amp;gt;:9119/api/status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If the client test fails, check firewall rules: &lt;code&gt;sudo ufw status&lt;/code&gt; or &lt;code&gt;sudo iptables -L&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Authentication fails
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Confirm &lt;code&gt;.env&lt;/code&gt; file has correct permissions: &lt;code&gt;ls -la ~/.hermes/.env&lt;/code&gt; (should be &lt;code&gt;600&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Verify the secret was generated: &lt;code&gt;grep HERMES_DASHBOARD_BASIC_AUTH_SECRET ~/.hermes/.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restart the service after config changes: &lt;code&gt;systemctl --user restart hermes-serve&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Gateway does not respond to messages
&lt;/h3&gt;

&lt;p&gt;The gateway is a separate process from the backend. If the desktop connects but messaging platforms do not work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check gateway status: &lt;code&gt;hermes gateway status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start the gateway if stopped: &lt;code&gt;hermes gateway start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Review logs: &lt;code&gt;hermes logs gateway -f&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/desktop" rel="noopener noreferrer"&gt;Hermes Agent Desktop App Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/configuration" rel="noopener noreferrer"&gt;Hermes Agent Configuration Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/messaging" rel="noopener noreferrer"&gt;Hermes Agent Messaging Gateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/" rel="noopener noreferrer"&gt;Hermes AI Assistant — Install, Setup, Workflow, and Troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-cli-cheatsheet/" rel="noopener noreferrer"&gt;Hermes Agent CLI cheat sheet — commands, flags, and slash shortcuts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/production-setup/" rel="noopener noreferrer"&gt;Hermes AI Assistant Skills for Real Production Setups&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is part of the &lt;a href="https://www.glukhov.org/ai-systems/" rel="noopener noreferrer"&gt;AI Systems&lt;/a&gt; cluster, which covers self-hosted assistants, retrieval architecture, local LLM infrastructure, and observability.&lt;/p&gt;

</description>
      <category>hermes</category>
      <category>selfhosting</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>GPUs for AI in 2026: NVIDIA, AMD, Intel Compared</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:14:41 +0000</pubDate>
      <link>https://dev.to/rosgluk/gpus-for-ai-in-2026-nvidia-amd-intel-compared-3gam</link>
      <guid>https://dev.to/rosgluk/gpus-for-ai-in-2026-nvidia-amd-intel-compared-3gam</guid>
      <description>&lt;p&gt;The AI hardware landscape has shifted significantly in 2026, with NVIDIA, AMD, and Intel all competing for developers who need GPUs capable of running local large language models and AI inference workloads.&lt;/p&gt;

&lt;p&gt;Choosing the right GPU for AI workloads requires looking beyond marketing numbers and focusing on the specifications that actually affect real-world performance. Memory capacity, memory bandwidth, and software ecosystem maturity consistently matter more than theoretical compute peaks when running transformer models locally.&lt;/p&gt;

&lt;p&gt;This comparison covers the most relevant workstation and prosumer GPUs available in mid-2026, including NVIDIA's Blackwell architecture (RTX 50-series), AMD's Radeon AI Pro R9700, and Intel's Arc Pro B70. The goal is to provide a practical reference for developers deciding which hardware best fits their model sizes, software stack, and budget constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which GPU specifications matter for AI workloads
&lt;/h2&gt;

&lt;p&gt;Marketing materials from GPU vendors emphasise AI TOPS and tensor performance, but these metrics rarely tell the complete story for local inference. The specifications below are ranked by their actual impact on running large language models.&lt;/p&gt;

&lt;h3&gt;
  
  
  VRAM capacity
&lt;/h3&gt;

&lt;p&gt;VRAM is typically the first limiting factor when running LLMs locally. A model cannot execute entirely on the GPU if it does not fit into available memory. Once model weights spill into system RAM, inference performance drops dramatically.&lt;/p&gt;

&lt;p&gt;Approximate VRAM requirements for common model sizes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model Size&lt;/th&gt;
&lt;th&gt;Recommended VRAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;8-12 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14B&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32B&lt;/td&gt;
&lt;td&gt;24-32 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70B&lt;/td&gt;
&lt;td&gt;48-64 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120B+&lt;/td&gt;
&lt;td&gt;Multiple GPUs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most homelab users, moving from 16 GB to 32 GB of VRAM provides a substantially larger practical benefit than increasing raw compute performance. A 32 GB GPU capable of running an entire model will often outperform a theoretically faster 16 GB GPU forced to offload tensors into system memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory bandwidth
&lt;/h3&gt;

&lt;p&gt;Memory bandwidth determines how quickly model weights can be streamed into compute units. Large transformer models continuously move massive amounts of data between VRAM and processing cores during inference.&lt;/p&gt;

&lt;p&gt;As models grow, bandwidth often becomes the dominant performance bottleneck. A card with higher bandwidth can outperform another GPU with significantly higher theoretical compute performance, particularly during prompt processing phases where the model reads through the entire context window.&lt;/p&gt;

&lt;h3&gt;
  
  
  FP32 compute
&lt;/h3&gt;

&lt;p&gt;FP32 throughput remains useful for scientific computing, simulation, rendering, and some AI preprocessing workloads. Modern inference engines rarely execute entirely in FP32 precision, relying instead on quantised formats like Q4_K_M or Q8_0. FP32 should be considered a secondary metric for AI inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI TOPS and tensor performance
&lt;/h3&gt;

&lt;p&gt;Every GPU vendor promotes AI TOPS as a headline number. These values are not directly comparable across vendors. NVIDIA, AMD, and Intel measure AI throughput differently, use different tensor hardware, and apply different assumptions regarding sparsity and numerical precision.&lt;/p&gt;

&lt;p&gt;AI TOPS should be viewed as an indication of peak theoretical capability rather than an expected LLM inference speed. Real-world token generation rates depend on model architecture, quantisation level, context length, and software optimisation — factors that TOPS numbers do not capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software ecosystem maturity
&lt;/h3&gt;

&lt;p&gt;Software support often determines whether hardware reaches its full potential. The current ecosystem landscape is approximately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Primary AI Stack&lt;/th&gt;
&lt;th&gt;Maturity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA&lt;/td&gt;
&lt;td&gt;CUDA, TensorRT&lt;/td&gt;
&lt;td&gt;Industry standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD&lt;/td&gt;
&lt;td&gt;ROCm, HIP, Vulkan&lt;/td&gt;
&lt;td&gt;Solid for PyTorch, llama.cpp, Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel&lt;/td&gt;
&lt;td&gt;oneAPI, SYCL, OpenVINO&lt;/td&gt;
&lt;td&gt;Improving rapidly, trailing peers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CUDA remains the industry standard with the broadest library support. ROCm has matured significantly over the past two years and now provides a functional experience for PyTorch, llama.cpp, and Ollama on Linux. Intel's oneAPI ecosystem continues to improve but still trails both NVIDIA and AMD in overall software maturity and community adoption.&lt;/p&gt;

&lt;p&gt;For a deeper look at NVIDIA-specific GPU analysis, see &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/comparing-nvidia-gpu-for-ai/" rel="noopener noreferrer"&gt;Comparing NVIDIA GPU Suitability for AI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete GPU comparison table
&lt;/h2&gt;

&lt;p&gt;The table below compares the most relevant workstation and enthusiast GPUs for AI workloads in 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;FP32 (TFLOPS)&lt;/th&gt;
&lt;th&gt;AI TOPS (INT8)&lt;/th&gt;
&lt;th&gt;TBP&lt;/th&gt;
&lt;th&gt;MSRP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5090&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;1792 GB/s&lt;/td&gt;
&lt;td&gt;104.6&lt;/td&gt;
&lt;td&gt;3352&lt;/td&gt;
&lt;td&gt;575 W&lt;/td&gt;
&lt;td&gt;$1799&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5080&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;960 GB/s&lt;/td&gt;
&lt;td&gt;56.3&lt;/td&gt;
&lt;td&gt;1801&lt;/td&gt;
&lt;td&gt;360 W&lt;/td&gt;
&lt;td&gt;$999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5070 Ti&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;896 GB/s&lt;/td&gt;
&lt;td&gt;43.9&lt;/td&gt;
&lt;td&gt;1406&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;$649&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5070&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;672 GB/s&lt;/td&gt;
&lt;td&gt;30.9&lt;/td&gt;
&lt;td&gt;494&lt;/td&gt;
&lt;td&gt;250 W&lt;/td&gt;
&lt;td&gt;$549&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5060 Ti 16GB&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;448 GB/s&lt;/td&gt;
&lt;td&gt;23.7&lt;/td&gt;
&lt;td&gt;614&lt;/td&gt;
&lt;td&gt;180 W&lt;/td&gt;
&lt;td&gt;$399&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 6000&lt;/td&gt;
&lt;td&gt;96 GB&lt;/td&gt;
&lt;td&gt;1792 GB/s&lt;/td&gt;
&lt;td&gt;125.0&lt;/td&gt;
&lt;td&gt;4000&lt;/td&gt;
&lt;td&gt;600 W&lt;/td&gt;
&lt;td&gt;$4999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 5000&lt;/td&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;1344 GB/s&lt;/td&gt;
&lt;td&gt;73.7&lt;/td&gt;
&lt;td&gt;2064&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;$2499&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 4500&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;896 GB/s&lt;/td&gt;
&lt;td&gt;54.9&lt;/td&gt;
&lt;td&gt;1577&lt;/td&gt;
&lt;td&gt;200 W&lt;/td&gt;
&lt;td&gt;$2500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 4000&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;td&gt;672 GB/s&lt;/td&gt;
&lt;td&gt;46.9&lt;/td&gt;
&lt;td&gt;1178&lt;/td&gt;
&lt;td&gt;145 W&lt;/td&gt;
&lt;td&gt;$1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 4000 SFF&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;td&gt;432 GB/s&lt;/td&gt;
&lt;td&gt;46.9&lt;/td&gt;
&lt;td&gt;770&lt;/td&gt;
&lt;td&gt;125 W&lt;/td&gt;
&lt;td&gt;$1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 2000&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;288 GB/s&lt;/td&gt;
&lt;td&gt;18.4&lt;/td&gt;
&lt;td&gt;592&lt;/td&gt;
&lt;td&gt;70 W&lt;/td&gt;
&lt;td&gt;$700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD Radeon AI Pro R9700&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;640 GB/s&lt;/td&gt;
&lt;td&gt;47.8&lt;/td&gt;
&lt;td&gt;766&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;$1299&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Arc Pro B70&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;608 GB/s&lt;/td&gt;
&lt;td&gt;22.94&lt;/td&gt;
&lt;td&gt;367&lt;/td&gt;
&lt;td&gt;230 W&lt;/td&gt;
&lt;td&gt;$949&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Key observations by segment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Consumer GPUs
&lt;/h3&gt;

&lt;p&gt;The RTX 5090 remains the fastest single-GPU solution for local AI development, combining exceptional memory bandwidth with the mature CUDA ecosystem. For users running large quantised models, it currently represents the highest-performance consumer option.&lt;/p&gt;

&lt;p&gt;The RTX 5080 and RTX 5070 Ti both offer 16 GB of VRAM, which is sufficient for most 7B-14B models but limits you when working with larger checkpoints. The RTX 5060 Ti 16GB variant is an interesting budget option — 16 GB of VRAM at $399 is compelling for entry-level AI workloads, though the narrower memory bus will impact throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workstation GPUs
&lt;/h3&gt;

&lt;p&gt;Within the workstation segment, AMD's Radeon AI Pro R9700 occupies an attractive middle ground. It delivers 32 GB of VRAM, competitive memory bandwidth, and a significantly lower purchase price than NVIDIA's professional offerings. For developers already comfortable with ROCm on Linux, it provides one of the strongest value propositions in 2026.&lt;/p&gt;

&lt;p&gt;Intel's Arc Pro B70 is particularly interesting because of its pricing. Although it offers lower compute performance than both NVIDIA and AMD, it provides the same 32 GB memory capacity while consuming less power. For users building cost-effective multi-GPU inference servers, the B70 deserves consideration — especially if the oneAPI ecosystem meets your software requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Professional GPUs
&lt;/h3&gt;

&lt;p&gt;NVIDIA's RTX PRO series dominates the professional segment, with the RTX PRO 6000 offering 96 GB of VRAM — unmatched by any competitor. For teams running very large models or multiple concurrent inference workloads, the RTX PRO 6000 and RTX PRO 5000 remain the safest choices, though at a premium price.&lt;/p&gt;

&lt;p&gt;For a real-world performance comparison across different hardware platforms, see &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/dgx-spark-vs-mac-studio-vs-rtx4080/" rel="noopener noreferrer"&gt;NVIDIA DGX Spark vs Mac Studio vs RTX-4080&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical hardware considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Physical dimensions and form factor
&lt;/h3&gt;

&lt;p&gt;GPU size varies significantly across product lines and affects compatibility with your case and cooling solution.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Approx. Length&lt;/th&gt;
&lt;th&gt;Slots&lt;/th&gt;
&lt;th&gt;Cooler Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;333 mm&lt;/td&gt;
&lt;td&gt;2.7×&lt;/td&gt;
&lt;td&gt;Triple-fan, blower or open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5080&lt;/td&gt;
&lt;td&gt;303 mm&lt;/td&gt;
&lt;td&gt;2.5×&lt;/td&gt;
&lt;td&gt;Dual/triple-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070 Ti&lt;/td&gt;
&lt;td&gt;280 mm&lt;/td&gt;
&lt;td&gt;2.4×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070&lt;/td&gt;
&lt;td&gt;245 mm&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5060 Ti&lt;/td&gt;
&lt;td&gt;200 mm&lt;/td&gt;
&lt;td&gt;1.8×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD R9700&lt;/td&gt;
&lt;td&gt;300 mm&lt;/td&gt;
&lt;td&gt;2.5×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Arc Pro B70&lt;/td&gt;
&lt;td&gt;267 mm&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;Single/dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 6000&lt;/td&gt;
&lt;td&gt;438 mm&lt;/td&gt;
&lt;td&gt;3.5×&lt;/td&gt;
&lt;td&gt;Blower, full-height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 5000&lt;/td&gt;
&lt;td&gt;438 mm&lt;/td&gt;
&lt;td&gt;3.5×&lt;/td&gt;
&lt;td&gt;Blower, full-height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000&lt;/td&gt;
&lt;td&gt;267 mm&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;Blower, low-profile option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000 SFF&lt;/td&gt;
&lt;td&gt;178 mm&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;Blower, half-height&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RTX PRO 6000 and 5000 are significantly longer than consumer cards and require full-height tower cases. The RTX PRO 4000 SFF is one of the few GPUs under 180 mm, making it suitable for compact workstation builds and rack-mounted servers.&lt;/p&gt;

&lt;p&gt;Consumer GPUs (RTX 50-series) use open-air coolers that exhaust heat into the case — adequate case airflow is essential. Workstation GPUs use blower-style coolers that exhaust heat directly out the rear, which is better for multi-GPU configurations and enclosed server environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power delivery and PSU requirements
&lt;/h3&gt;

&lt;p&gt;TBP (Total Board Power) is the GPU's maximum power draw, but actual system requirements depend on transient spikes and CPU overhead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;TBP&lt;/th&gt;
&lt;th&gt;Recommended PSU&lt;/th&gt;
&lt;th&gt;Power Connectors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;575 W&lt;/td&gt;
&lt;td&gt;1000 W+&lt;/td&gt;
&lt;td&gt;12V-2x6 (20-pin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5080&lt;/td&gt;
&lt;td&gt;360 W&lt;/td&gt;
&lt;td&gt;750 W&lt;/td&gt;
&lt;td&gt;12V-2x6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070 Ti&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;650 W&lt;/td&gt;
&lt;td&gt;8-pin + 8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070&lt;/td&gt;
&lt;td&gt;250 W&lt;/td&gt;
&lt;td&gt;600 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5060 Ti&lt;/td&gt;
&lt;td&gt;180 W&lt;/td&gt;
&lt;td&gt;550 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD R9700&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;650 W&lt;/td&gt;
&lt;td&gt;8-pin + 8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Arc Pro B70&lt;/td&gt;
&lt;td&gt;230 W&lt;/td&gt;
&lt;td&gt;550 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 6000&lt;/td&gt;
&lt;td&gt;600 W&lt;/td&gt;
&lt;td&gt;1000 W+&lt;/td&gt;
&lt;td&gt;12V-2x6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 5000&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;650 W&lt;/td&gt;
&lt;td&gt;8-pin + 8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000&lt;/td&gt;
&lt;td&gt;145 W&lt;/td&gt;
&lt;td&gt;500 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000 SFF&lt;/td&gt;
&lt;td&gt;125 W&lt;/td&gt;
&lt;td&gt;450 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 2000&lt;/td&gt;
&lt;td&gt;70 W&lt;/td&gt;
&lt;td&gt;400 W&lt;/td&gt;
&lt;td&gt;PCIe slot only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RTX 5090 and RTX PRO 6000 both exceed 575W TBP and require the newer 12V-2x6 connector (20-pin). Ensure your PSU supports this connector natively — adapter cables from multiple 8-pin connectors are not recommended for cards above 450W due to transient power spikes that can exceed rated capacity momentarily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thermal characteristics and sustained workloads
&lt;/h3&gt;

&lt;p&gt;AI inference workloads keep the GPU under sustained load, unlike gaming which has variable utilisation. This affects thermal behaviour significantly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RTX 5090 at 575W&lt;/strong&gt;: Expect GPU temperatures of 72-78°C under sustained inference. The higher TBP means more heat dissipation is required — a case with positive static pressure and quality filters is recommended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTX 5080 at 360W&lt;/strong&gt;: Runs cooler, typically 65-72°C. More manageable for standard mid-tower cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workstation GPUs (blower)&lt;/strong&gt;: RTX PRO series exhaust heat directly out the case, keeping case temperatures lower. GPU temperatures may read higher (75-82°C) but this is by design — the blower cooler trades GPU temperature for lower case temperature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-power options&lt;/strong&gt;: RTX PRO 2000 at 70W and RTX PRO 4000 SFF at 125W are suitable for passive or low-fan-speed cooling, making them ideal for always-on inference servers where noise matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For multi-GPU setups, blower-style coolers (workstation GPUs) are strongly preferred over open-air consumer coolers, as the second GPU would otherwise pull hot air from the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  PCIe lanes and bandwidth
&lt;/h3&gt;

&lt;p&gt;GPU performance can be limited by PCIe lane count. A GPU plugged into a x8 or x4 slot will experience reduced memory bandwidth compared to a full x16 connection. For multi-GPU setups, understand how PCIe lanes are distributed across your motherboard. See &lt;a href="https://www.glukhov.org/llm-performance/hardware/llm-performance-and-pci-lanes/" rel="noopener noreferrer"&gt;LLM Performance and PCIe Lanes&lt;/a&gt; for detailed analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-GPU setups
&lt;/h3&gt;

&lt;p&gt;When a single GPU cannot fit your model, multi-GPU configurations become necessary. NVIDIA NVLink (where supported) and PCIe-based model parallelism are the primary approaches. The &lt;a href="https://www.glukhov.org/hardware/ai/building-team-ai-infrastructure-on-consumer-hardware/" rel="noopener noreferrer"&gt;AI Infrastructure on Consumer Hardware&lt;/a&gt; guide covers multi-GPU deployment strategies in depth.&lt;/p&gt;

&lt;p&gt;Note that AMD and Intel GPUs have limited multi-GPU inference support in most frameworks. If you plan to scale with multiple GPUs, NVIDIA is currently the only practical option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There is no universally best GPU for AI workloads. The right choice depends on your software stack, budget, and the size of the models you intend to run.&lt;/p&gt;

&lt;p&gt;NVIDIA's Blackwell family remains the benchmark for inference performance, thanks to outstanding memory bandwidth and the maturity of CUDA and TensorRT. AMD's Radeon AI Pro R9700 has established itself as a compelling workstation option, offering an excellent balance between price, memory capacity, and compute performance. Intel's Arc Pro B70 proves that affordable 32 GB workstation GPUs are now a reality, though its software ecosystem continues to mature.&lt;/p&gt;

&lt;p&gt;The most important lesson from 2026 is that AI hardware should no longer be evaluated using gaming benchmarks. For modern LLM inference, VRAM capacity, memory bandwidth, and software support consistently have a greater impact on real-world performance than theoretical AI TOPS alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/benchmarks/comparing-nvidia-gpu-for-ai/" rel="noopener noreferrer"&gt;Comparing NVIDIA GPU Suitability for AI&lt;/a&gt; — NVIDIA-specific GPU analysis with detailed CUDA core and tensor core comparisons&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/hardware/ai/building-team-ai-infrastructure-on-consumer-hardware/" rel="noopener noreferrer"&gt;AI Infrastructure on Consumer Hardware&lt;/a&gt; — Full-stack guide to deploying self-hosted AI with consumer GPUs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/benchmarks/dgx-spark-vs-mac-studio-vs-rtx4080/" rel="noopener noreferrer"&gt;NVIDIA DGX Spark vs Mac Studio vs RTX-4080&lt;/a&gt; — Real-world Ollama performance benchmarks across hardware platforms&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/hardware/llm-performance-and-pci-lanes/" rel="noopener noreferrer"&gt;LLM Performance and PCIe Lanes&lt;/a&gt; — How PCIe configuration affects LLM inference performance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-cheatsheet/" rel="noopener noreferrer"&gt;Ollama Cheatsheet&lt;/a&gt; — Command reference and tips for Ollama model serving&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/hardware/gpu/rtx-5880-ada/" rel="noopener noreferrer"&gt;Quadro RTX 5880 Ada Review&lt;/a&gt; — Review of the 48GB workstation GPU alternative&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/benchmarks/best-llm-on-16gb-vram-gpu/" rel="noopener noreferrer"&gt;Best LLM on 16 GB VRAM GPU&lt;/a&gt; — llama.cpp benchmarks for models on 16 GB VRAM&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gpu</category>
      <category>ai</category>
      <category>nvidia</category>
      <category>hardware</category>
    </item>
  </channel>
</rss>
