<?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>RTX 5090 Price in Australia: Listings and History</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 01 Nov 2026 23:51:35 +0000</pubDate>
      <link>https://dev.to/rosgluk/rtx-5090-price-in-australia-listings-and-history-30o8</link>
      <guid>https://dev.to/rosgluk/rtx-5090-price-in-australia-listings-and-history-30o8</guid>
      <description>&lt;p&gt;On 29/10/2026 the cheapest in-stock RTX 5090s averaged A$12,249, and the RTX 5080s averaged A$2,832. A shop with no card in stock is left out of the average.&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://www.glukhov.org/hardware/gpu/nvidia-rtx-5080-rtx-5090-prices-july-2025/" rel="noopener noreferrer"&gt;July 2025 check&lt;/a&gt; through the &lt;a href="https://www.glukhov.org/hardware/memory/ram-and-gpu-price-increase/" rel="noopener noreferrer"&gt;January 2026 check&lt;/a&gt;, the published figure is the average of the cheapest in-stock card at Centrecom, PC Case Gear, and Scorptec. On 29/10/2026 the RTX 5090 floors were Centrecom A$12,999 and Scorptec A$11,499, and PC Case Gear had none in stock. The RTX 5080 floors were Centrecom A$2,899, PC Case Gear A$2,899, and Scorptec A$2,699, and that average is rounded to the nearest dollar.&lt;/p&gt;

&lt;p&gt;The January 2026 RTX 5080 average on this same measure was A$1,899. Memory capacity, bandwidth, and the Arc Pro B70 and Radeon AI Pro R9700 comparison stay on &lt;a href="https://www.glukhov.org/hardware/ai/gpu-comparison-ai-workloads-2026-nvidia-amd-intel/" rel="noopener noreferrer"&gt;GPUs for AI in 2026: NVIDIA, AMD, Intel Compared&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retailer listings
&lt;/h2&gt;

&lt;p&gt;These rows are the 29/10/2026 check. A sold-out row is the price on the listing, not a card that could be added to a cart. Where several cards shared the cheapest in-stock price, each of those cards is a row.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checked&lt;/th&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Price (AUD)&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Retailer&lt;/th&gt;
&lt;th&gt;Availability&lt;/th&gt;
&lt;th&gt;Listing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;ASUS TUF Gaming GeForce RTX 5090 32GB&lt;/td&gt;
&lt;td&gt;12,999&lt;/td&gt;
&lt;td&gt;Cheapest in-stock at this shop&lt;/td&gt;
&lt;td&gt;Centrecom&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.centrecom.com.au/asus-tuf-gaming-geforce-rtx-5090-32gb-gddr7-graphics-card" rel="noopener noreferrer"&gt;Centrecom&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Palit GeForce RTX 5090 GameRock, 32GB&lt;/td&gt;
&lt;td&gt;11,499&lt;/td&gt;
&lt;td&gt;Cheapest in-stock at this shop&lt;/td&gt;
&lt;td&gt;Scorptec&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.scorptec.com.au/product/graphics-cards/nvidia/116255-ne75090019r5-gb2020g" rel="noopener noreferrer"&gt;Scorptec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Palit GeForce RTX 5090 GameRock OC, 32GB&lt;/td&gt;
&lt;td&gt;11,499&lt;/td&gt;
&lt;td&gt;Named in-stock listing&lt;/td&gt;
&lt;td&gt;Scorptec&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.scorptec.com.au/product/graphics-cards/nvidia/121949-ne75090s19r5-gb2020g" rel="noopener noreferrer"&gt;Scorptec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;ASUS ProArt GeForce RTX 5090 32GB OC&lt;/td&gt;
&lt;td&gt;12,999&lt;/td&gt;
&lt;td&gt;Named in-stock listing&lt;/td&gt;
&lt;td&gt;Scorptec&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.scorptec.com.au/product/graphics-cards/nvidia/126060-proart-rtx5090-o32g" rel="noopener noreferrer"&gt;Scorptec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;ASUS GeForce RTX 5090 TUF Gaming 32GB&lt;/td&gt;
&lt;td&gt;10,499&lt;/td&gt;
&lt;td&gt;Listed price, sold out&lt;/td&gt;
&lt;td&gt;PC Case Gear&lt;/td&gt;
&lt;td&gt;Sold out&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.pccasegear.com/products/68108/asus-geforce-rtx-5090-tuf-gaming-gddr7-32gb" rel="noopener noreferrer"&gt;PC Case Gear&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;MSI GeForce RTX 5090 Ventus 3X OC 32GB&lt;/td&gt;
&lt;td&gt;10,499&lt;/td&gt;
&lt;td&gt;Listed price, sold out&lt;/td&gt;
&lt;td&gt;PC Case Gear&lt;/td&gt;
&lt;td&gt;Sold out&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.pccasegear.com/products/68151/msi-geforce-rtx-5090-ventus-3x-oc-gddr7-32gb" rel="noopener noreferrer"&gt;PC Case Gear&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Palit GeForce RTX 5080 Infinity 3, 16GB&lt;/td&gt;
&lt;td&gt;2,699&lt;/td&gt;
&lt;td&gt;Cheapest in-stock at this shop&lt;/td&gt;
&lt;td&gt;Scorptec&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.scorptec.com.au/product/graphics-cards/nvidia/126954-ne75080019t2-gb2035s" rel="noopener noreferrer"&gt;Scorptec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Palit GeForce RTX 5080 Infinity 3, 16GB&lt;/td&gt;
&lt;td&gt;2,899&lt;/td&gt;
&lt;td&gt;Cheapest in-stock at this shop&lt;/td&gt;
&lt;td&gt;Centrecom&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.centrecom.com.au/palit-geforce-rtx-5080-infinity-3-16gb-gddr7-graphics-card" rel="noopener noreferrer"&gt;Centrecom&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Palit GeForce RTX 5080 GamingPro OC V1, 16GB&lt;/td&gt;
&lt;td&gt;2,699&lt;/td&gt;
&lt;td&gt;Named in-stock listing&lt;/td&gt;
&lt;td&gt;Scorptec&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.scorptec.com.au/product/graphics-cards/nvidia/128679-ne75080s19t2-gb2031y" rel="noopener noreferrer"&gt;Scorptec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;ZOTAC GAMING GeForce RTX 5080 Solid Core OC, 16GB&lt;/td&gt;
&lt;td&gt;2,699&lt;/td&gt;
&lt;td&gt;Named in-stock listing&lt;/td&gt;
&lt;td&gt;Scorptec&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.scorptec.com.au/product/graphics-cards/nvidia/125645-zt-b50800j2-10p" rel="noopener noreferrer"&gt;Scorptec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Gigabyte GeForce RTX 5080 Windforce OC V2 16GB&lt;/td&gt;
&lt;td&gt;2,899&lt;/td&gt;
&lt;td&gt;Cheapest in-stock at this shop&lt;/td&gt;
&lt;td&gt;PC Case Gear&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.pccasegear.com/products/74326/gigabyte-geforce-rtx-5080-windforce-oc-v2-gddr7-16gb" rel="noopener noreferrer"&gt;PC Case Gear&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;Palit GeForce RTX 5080 Infinity 3 16GB&lt;/td&gt;
&lt;td&gt;2,899&lt;/td&gt;
&lt;td&gt;Named in-stock listing&lt;/td&gt;
&lt;td&gt;PC Case Gear&lt;/td&gt;
&lt;td&gt;In stock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.pccasegear.com/products/73434/palit-geforce-rtx-5080-infinity-3-gddr7-16gb" rel="noopener noreferrer"&gt;PC Case Gear&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PC Case Gear's RTX 5090 category had eighteen listings, and every one of them was sold out. The two sold-out rows are the lowest of those prices. Scorptec's in-stock RTX 5090s start at the Palit GameRock rows, and the ASUS ProArt OC row is the highest in-stock price in this check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Earlier checks
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.glukhov.org/hardware/gpu/nvidia-rtx-5080-rtx-5090-prices-australia/" rel="noopener noreferrer"&gt;June 2025 check&lt;/a&gt; is a shop minimum for the RTX 5090, and the July page carries the June RTX 5080 average forward. From July 2025 through January 2026 the figure is the three-shop average. &lt;a href="https://www.glukhov.org/hardware/gpu/nvidia-rtx-5080-rtx-5090-prices-october-2025/" rel="noopener noreferrer"&gt;October 2025&lt;/a&gt; is the low of that RTX 5090 average, and &lt;a href="https://www.glukhov.org/hardware/gpu/nvidia-rtx-5080-rtx-5090-australia-prices-november-2025/" rel="noopener noreferrer"&gt;November 2025&lt;/a&gt; is the next row.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.glukhov.org/hardware/gpu/nvidia-rtx-5080-rtx-5090-prices-march-2026/" rel="noopener noreferrer"&gt;March 2026 check&lt;/a&gt; uses an entry-to-premium range rather than that average, and it does not publish an RTX 5080 average. NVIDIA's Australian list prices from the June check are the first row of the table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;RTX 5090 (AUD)&lt;/th&gt;
&lt;th&gt;RTX 5080 (AUD)&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Launch list&lt;/td&gt;
&lt;td&gt;4,039&lt;/td&gt;
&lt;td&gt;2,019&lt;/td&gt;
&lt;td&gt;NVIDIA Australia list, from the June 2025 check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June 2025&lt;/td&gt;
&lt;td&gt;5,299&lt;/td&gt;
&lt;td&gt;2,009&lt;/td&gt;
&lt;td&gt;5090: same minimum at five shops. 5080: average of cheapest in-stock, as carried into the July check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July 2025&lt;/td&gt;
&lt;td&gt;4,532&lt;/td&gt;
&lt;td&gt;1,985&lt;/td&gt;
&lt;td&gt;Average of cheapest in-stock at Centrecom, PC Case Gear, and Scorptec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;October 2025&lt;/td&gt;
&lt;td&gt;4,282&lt;/td&gt;
&lt;td&gt;1,782&lt;/td&gt;
&lt;td&gt;Same three-shop average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;November 2025&lt;/td&gt;
&lt;td&gt;4,832&lt;/td&gt;
&lt;td&gt;1,786&lt;/td&gt;
&lt;td&gt;Same three-shop average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;January 2026&lt;/td&gt;
&lt;td&gt;5,566&lt;/td&gt;
&lt;td&gt;1,899&lt;/td&gt;
&lt;td&gt;Same three-shop average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;March 2026&lt;/td&gt;
&lt;td&gt;5,999–7,500&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Entry-to-premium range for the 5090. No 5080 average in that check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29/10/2026&lt;/td&gt;
&lt;td&gt;12,249&lt;/td&gt;
&lt;td&gt;2,832&lt;/td&gt;
&lt;td&gt;5090: average of the shops with stock (PC Case Gear had none). 5080: average of all three shops, nearest dollar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The January TL;DR rounds the three shop floors (Centrecom A$5,499, PC Case Gear A$5,799, Scorptec A$5,399) to A$5,566. The March range is a different measurement, so it is not a step on the average.&lt;/p&gt;

&lt;p&gt;On the same day as the 29/10/2026 GPU listings, PC Case Gear listed an ASUS Ascent GX10 with 128 GB and 1 TB at A$9,649, with stock at the supplier rather than in the warehouse. That workstation check, and the January 2026 DGX Spark prices, are on &lt;a href="https://www.glukhov.org/hardware/ai/dgx-spark-pricing-in-australia/" rel="noopener noreferrer"&gt;DGX Spark Pricing in Australia&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&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;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gpu</category>
      <category>ai</category>
      <category>nvidia</category>
      <category>hardware</category>
    </item>
    <item>
      <title>Decision LLMs: When a Model Should Decide — and When It Shouldn't</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:59:29 +0000</pubDate>
      <link>https://dev.to/rosgluk/decision-llms-when-a-model-should-decide-and-when-it-shouldnt-5m0</link>
      <guid>https://dev.to/rosgluk/decision-llms-when-a-model-should-decide-and-when-it-shouldnt-5m0</guid>
      <description>&lt;p&gt;Ask practitioners what breaks in agent systems and the answer is rarely capability. The failure is quieter: the model decides, and nobody can tell sound from fluent.&lt;/p&gt;

&lt;p&gt;A model output is a sample. A decision has properties a sample does not have — determinism for a fixed input, invariants it must satisfy, an audit trail that says why this and not the alternative. When those properties are missing, the "decision" is a high-confidence guess wearing the clothing of one.&lt;/p&gt;

&lt;p&gt;This article gives that gap a mechanical treatment: a taxonomy of five decision roles, a rule for routing each decision to the smallest model competent for it, and the verification machinery that turns "the agent decided X" into something you can later prove was sound. It closes with a rubric you can run against any action in your own system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"For local agents the gap I keep hitting is not capability, it is verification. The agent says it read a file, called a tool, or made a decision…" — r/LocalLLaMA, "Best Local Agents — Jun 2026"&lt;/p&gt;

&lt;p&gt;"When an LLM agent fails, it's a black box and nobody knows why. The real bottleneck to adoption isn't getting a model from 92% to 96% accuracy — [it's knowing why it decided X]." — r/AI_Agents, "The gap between decision and execution"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The community has converged on three instincts: don't trust the model with the final call, route the decision to the smallest competent model, and make the decision explainable. What has been missing is the framework that makes the three actionable at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "it answers, but does it decide?"
&lt;/h2&gt;

&lt;p&gt;A model output is a token distribution collapsed to one sequence, and the same prompt run twice can produce two sequences that diverge on exactly the token that matters. A decision, by contrast, is deterministic for a fixed input, satisfies explicit invariants, and leaves an audit trail recording which branch was taken and why.&lt;/p&gt;

&lt;p&gt;When a model "decides" without machinery around it, none of those properties exist. There is no invariant the output is obligated to respect, no log of the branch taken, no way to replay the decision and check it held. The gap is not intelligence — it is that a probabilistic output has been placed in a slot designed for a deterministic one.&lt;/p&gt;

&lt;p&gt;The practitioner instinct is to keep the model out of the decision entirely:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The LLM never makes final decisions in the system. Its role is purely: logic refinement, stress testing, scenario generation, explanation." — r/AI_Agents, "Using an LLM as a 'reasoning agent' for validating price…"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the right posture stated as a maxim. To apply it, you need a way to classify which role the model should hold for a given action — which is what the taxonomy below provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five decision roles
&lt;/h2&gt;

&lt;p&gt;Every decision a system makes can be assigned to one of five roles. The role determines how much the model's output can be trusted, how it is audited, how it fails, and whether a human belongs in the loop.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;The model…&lt;/th&gt;
&lt;th&gt;Trust level&lt;/th&gt;
&lt;th&gt;Auditability&lt;/th&gt;
&lt;th&gt;Typical failure mode&lt;/th&gt;
&lt;th&gt;Human in the loop?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Decider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commits the call&lt;/td&gt;
&lt;td&gt;Lowest — must be constrained&lt;/td&gt;
&lt;td&gt;Must emit a decision record&lt;/td&gt;
&lt;td&gt;High-confidence wrong branch&lt;/td&gt;
&lt;td&gt;Yes, for irreversible/high-stakes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recommender&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Produces options + a ranking&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Ranked list is inspectable&lt;/td&gt;
&lt;td&gt;Confident wrong option&lt;/td&gt;
&lt;td&gt;Optional; human picks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evaluator (judge)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scores an input against a rubric&lt;/td&gt;
&lt;td&gt;Medium — rubric-bound&lt;/td&gt;
&lt;td&gt;Score + rubric trace&lt;/td&gt;
&lt;td&gt;Rubric drift; self-preference bias&lt;/td&gt;
&lt;td&gt;No, but rubric is versioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Produces the artifact&lt;/td&gt;
&lt;td&gt;Not applicable — it is content&lt;/td&gt;
&lt;td&gt;Content review&lt;/td&gt;
&lt;td&gt;Off-spec, hallucinated specifics&lt;/td&gt;
&lt;td&gt;Review of output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Router&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Selects the path/model for the task&lt;/td&gt;
&lt;td&gt;Medium — deterministic mapping&lt;/td&gt;
&lt;td&gt;Routing log&lt;/td&gt;
&lt;td&gt;Misrouted tier&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the table as a constraint, not a menu. Most broken systems share one defect: a single model fills all five roles at once, so a role that demands determinism (decider) is served by a component optimized for fluency (generator). Where the model acts as generator, none of the decision machinery is needed — content review is. The failure to avoid is conflating these: "the agent decided to delete the environment" is usually a missing approval gate, not a decision-quality problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route the decision to the smallest competent model
&lt;/h2&gt;

&lt;p&gt;Once the role is fixed, the routing question is which model tier serves it. Practitioners report the payoff directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Model routing: Haiku for classification/routing decisions, Sonnet for execution, Opus only for design/planning. Cost dropped ~60% once I stopped using Opus for everything." — r/AI_Agents, "What is your full AI Agent stack in 2026"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That number comes from treating model selection as a per-role decision instead of a per-system one. A routing call — "is this an email triage or a refund approval?" — is a classification, and classification does not need a frontier model; it needs a small, fast, low-temperature model, because it runs thousands of times. The tier mechanics — capability-based, cost-aware, and latency-aware selection, plus fallback chains — are covered in &lt;a href="https://www.glukhov.org/llm-architecture/model-routing/model-routing-strategies/" rel="noopener noreferrer"&gt;Model Routing: Stop Using One Model for Everything&lt;/a&gt;; the orchestration patterns around multiple models are in &lt;a href="https://www.glukhov.org/llm-architecture/model-routing/multi-model-system-design/" rel="noopener noreferrer"&gt;Multi-Model System Design&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The addition the decision framing contributes is &lt;strong&gt;confidence-based escalation&lt;/strong&gt;, which makes "smallest competent" measured rather than assumed. Start on the smallest tier. If the model emits a calibrated confidence — or a structured self-assessment — and it clears a threshold, stop. If it does not, escalate one tier and record the escalation. Competence is then a property verified per call, not a property assumed per model.&lt;/p&gt;

&lt;p&gt;One constraint stays absolute regardless of tier: the router role itself must not be a free language-model call. A deterministic spine — rules on task type, payload size, cost budget — selects the branch, and the model fills only the leaf decisions inside it. The spine is auditable and unit-testable; the model is contained to what it is actually good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the decision verifiable
&lt;/h2&gt;

&lt;p&gt;This is where the "agent says it did X" gap closes. The fix is to make the decision a first-class structured artifact rather than a side effect of a chat turn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A structured decision store.&lt;/strong&gt; Every decision the model is allowed to make is written to a table, not a log line: the input, the role, the model and tier, the confidence, the alternatives considered, the timestamp. A decision is something you can &lt;code&gt;SELECT&lt;/code&gt; later. If it lives only in a conversation transcript, it is a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A decision log, separate from the execution log.&lt;/strong&gt; The execution log says what happened; the decision log says what was decided and on what basis. The separation matters because the failure is usually in the decision, not the execution — the agent executed the deletion perfectly; the bug was deciding to delete. The mechanics of structured, append-only logging are the same ones used for compliance audit trails in &lt;a href="https://www.glukhov.org/llm-architecture/guardrails/llm-guardrails-in-practice/" rel="noopener noreferrer"&gt;LLM Guardrails in Practice&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invariants and assertions.&lt;/strong&gt; For decider and evaluator roles, the decision must satisfy invariants checked after the model emits it, in deterministic code. A price recommender can emit anything; the invariant "recommended price must be within [floor, ceiling]" is enforced by code. The model proposes; the invariant disposes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-approval gates at the irreversibility boundary.&lt;/strong&gt; The gate is a property of the action's blast radius, not of the model's confidence. A refund under a threshold is auto-approvable; one over it is not. A 99%-confident model still cannot approve an irreversible action it has no audit record for.
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Action requires&amp;lt;br/&amp;gt;a decision"] --&amp;gt; B{"Classify&amp;lt;br/&amp;gt;the role"}
    B --&amp;gt; C["Route to smallest&amp;lt;br/&amp;gt;competent tier"]
    C --&amp;gt; D["Emit structured&amp;lt;br/&amp;gt;decision record"]
    D --&amp;gt; E{"Invariants&amp;lt;br/&amp;gt;hold?"}
    E -- "yes" --&amp;gt; F{"Irreversible or&amp;lt;br/&amp;gt;high-stakes?"}
    E -- "no" --&amp;gt; G["Reject, escalate,&amp;lt;br/&amp;gt;and log"]
    F -- "yes" --&amp;gt; H["Human approval gate"]
    F -- "no" --&amp;gt; I["Execute"]
    H -- "approved" --&amp;gt; I
    H -- "rejected" --&amp;gt; G&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The through-line: the 92%→96% accuracy push is not the bottleneck — explainability is. A system at 92% with every decision logged, invariant-checked, and gated at the boundary ships safely, because the 8% of wrong decisions are findable before they do damage. A system at 99% with a black-box decision process does not, because the 1% is a live incident with no forensic record. Where the gate itself lives in an assistant — which component holds approval authority and how requests reach it — is laid out in &lt;a href="https://www.glukhov.org/ai-systems/architecture/ai-assistant-architecture/" rel="noopener noreferrer"&gt;AI Assistant Architecture: LLM, Memory, Tools, Routing, Observability&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked examples
&lt;/h2&gt;

&lt;p&gt;Three systems, one role assignment each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price validation — the model as recommender, never decider.&lt;/strong&gt; An agent reviews a supplier price change and produces a ranked list: accept, negotiate at X, reject. The final call is a decider slot filled by deterministic rules: below the cost floor, auto-accept; above the ceiling, route to a human; in between, apply the invariant "negotiated price must beat last accepted by Y%." The model never approves its own recommendation, and the recommendation sits in the decision store with its rubric trace if it goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent task routing — the spine as decider, the model at the leaves.&lt;/strong&gt; An inbound request (email, ticket, command) is assigned a path by payload shape and intent category — deterministic, unit-testable. A small model fills only the leaf decision inside the branch. A misrouting is a blast-radius event, which is exactly why the branch selection must be deterministic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM-as-judge — the model as evaluator, the rubric as the constraint.&lt;/strong&gt; One model scores another's output against a versioned rubric, and the score plus rubric trace are stored, not just the number. The known failure mode is self-preference bias — a model scoring its own generation style higher — and the mitigation is structural: version the rubric so a model is never silently scoring against criteria it shaped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest boundary
&lt;/h2&gt;

&lt;p&gt;There is a class of decision an LLM must not make, and it is not a question of scale: actions that are &lt;strong&gt;irreversible, high-stakes, and have no fallback&lt;/strong&gt; — data deletion, fund movement, external commitments. Verification is impossible for an action you cannot roll back, which is why "autonomous agent" keeps hitting the same wall regardless of model quality.&lt;/p&gt;

&lt;p&gt;The human-in-the-loop that is not theater sits at exactly that boundary and nowhere else. A human reviewing every low-stakes decision adds friction and no safety; a human absent from every high-stakes decision is the incident. The same principle appears in a narrower form in agent memory: &lt;a href="https://www.glukhov.org/ai-systems/memory/self-reinforcing-memory-loops/" rel="noopener noreferrer"&gt;self-reinforcing memory loops&lt;/a&gt; gate candidate memory writes behind explicit human approval before anything reaches durable state — the gate sized to what the write can break. For multi-agent systems, the corresponding question is who may act on whose behalf, and the identity, delegation, and audit-trail machinery for it is in &lt;a href="https://www.glukhov.org/llm-architecture/guardrails/a2a-mcp-agent-security/" rel="noopener noreferrer"&gt;A2A and MCP Agent Security&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rubric
&lt;/h2&gt;

&lt;p&gt;Run this against any action in your system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classify the role.&lt;/strong&gt; Decider, recommender, evaluator, generator, or router? An unnamed role is a bug — a model in a decider slot wearing a generator's confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the tier for that role.&lt;/strong&gt; Smallest competent, with confidence-based escalation so competence is measured per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define the verification.&lt;/strong&gt; Which invariants must hold? What goes in the decision store? What is the audit trace? A role with no verification path is a decider by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the human gate from blast radius, not confidence.&lt;/strong&gt; Irreversible or high-stakes → gated. Otherwise → auto, with the decision logged so it is findable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same discipline, applied to engineering choices rather than runtime actions, is what decision records do — &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; captures why a choice was made so it can be reviewed later; the rubric above does the same for the decisions a running system makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming: the decider as a product
&lt;/h2&gt;

&lt;p&gt;The framework predicts its own commoditization: once "route the decision to the smallest competent model" became a pattern, the next step was a model class that does nothing but decide. That step has now landed.&lt;/p&gt;

&lt;p&gt;Through October 2026, several vendors shipped models built on a shared recipe — a frozen Qwen backbone, a rank-256 LoRA adapter, and a joint schema head — that emit &lt;strong&gt;typed decisions in a single forward pass&lt;/strong&gt;: a choice from an enumerated set, a yes/no, or a score, with calibrated probabilities and no free-form text. There is no chain of thought to wait for, because there is no generation. Vendor-reported latency sits between roughly 8 and 210 ms across the class — from the smallest variant (d1-3B, about 8 ms) up to the largest (d3, about 209 ms) — and the footprints are small-GPU territory: d1-3B at about 1.67 GB, d3-flash at about 6.6 GB at Q4_K_M. Support also moved fast: llama.cpp merged native architecture support for several of these models within roughly two weeks of release.&lt;/p&gt;

&lt;p&gt;Two consequences for the framework above. First, the routing rule gets a hardware expression it did not have before: the decider role can be served by a dedicated small model instead of a prompt mode of a general one, which makes the smallest-competent-tier decision partly automatic — the tier either exists for your decision type or it does not. Second, nothing about verification changes. A typed decision in 8 ms still needs the decision store, the invariants, and the gate at the irreversibility boundary; what the model class removes is generation cost and non-determinism, not the audit burden. Faster decisions multiply the number of decision records to audit — the bottleneck was never the model's speed, and it is not the model's intelligence either.&lt;/p&gt;

&lt;p&gt;The other directions to watch are quieter. Tool-calling reliability for small models is approaching saturation — post-training on tool use is the cheapest high-impact step in the stack, which shifts attention to the rows above. Reliable planning horizons keep extending, which makes the decider role reachable for longer tasks and grows the verification burden with the horizon. Both feed the same conclusion: the scarce resource in agent systems is decisions you can later prove were sound, and everything else is catching up to that.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>Qwen 4 Is Coming: What's Confirmed and What It Means for Local AI</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 24 Oct 2026 22:58:09 +0000</pubDate>
      <link>https://dev.to/rosgluk/qwen-4-is-coming-whats-confirmed-and-what-it-means-for-local-ai-2b8m</link>
      <guid>https://dev.to/rosgluk/qwen-4-is-coming-whats-confirmed-and-what-it-means-for-local-ai-2b8m</guid>
      <description>&lt;p&gt;Alibaba confirmed at Apsara that Qwen 4 is in training, no date attached, while community timing signals point at a launch in weeks rather than months.&lt;/p&gt;

&lt;p&gt;For local AI, the consequential part is not the announcement but the 27B tier. Alibaba has shipped a capable dense model in that class in every recent generation, and the current one already runs on a single consumer GPU.&lt;/p&gt;

&lt;p&gt;This article separates what Alibaba has actually confirmed from cadence inference, looks at what each 27B generation gained and what that implies for the next one, examines the architecture preview that can already be run, and works through the hardware and licensing picture for self-hosting. If your shortlist extends past this one family, the &lt;a href="https://www.glukhov.org/ai-models/" rel="noopener noreferrer"&gt;AI Models&lt;/a&gt; pages follow the rest of the landscape: architecture shifts, other model lines, and where the quantization tradeoffs sit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Alibaba has confirmed
&lt;/h2&gt;

&lt;p&gt;On 22 September 2026, at the Apsara Conference in Hangzhou, Alibaba confirmed that Qwen 4 is in training. Qwen lead Liu Dayiheng described it as built on a "new-generation architecture" and arriving "very soon" — no date, no specifications, no benchmark results, and no licensing terms.&lt;/p&gt;

&lt;p&gt;The widely quoted 5–10 trillion parameter figures refer to the longer-term Qwen 4.5 and Qwen 5 roadmap, not to confirmed Qwen 4 specifications. The current flagship, Qwen3.8-Max, is a ~2.4T-parameter MoE with roughly 95B active parameters and a 1M-token context window.&lt;/p&gt;

&lt;p&gt;Two further items from the same announcement matter for the release itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3.8-Flash-Next&lt;/strong&gt; (released late August 2026) is explicitly described in Alibaba's own materials as "a preview of the Qwen 4 architecture" — the one Qwen 4-adjacent model that can be run today. More on what it demonstrates below.&lt;/li&gt;
&lt;li&gt;The full-stack roadmap includes the &lt;strong&gt;Zhenwu V900&lt;/strong&gt; AI accelerator from T-Head, a target of &lt;strong&gt;20+ GW&lt;/strong&gt; of Alibaba Cloud data-center capacity by 2032, and Qwen-Audio 3.1, Qwen3.8-LiveTranslate, and Qwen-Image 3.1 later in the year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything beyond this — variant names, dates, benchmark scores — originates from community threads, tracker pages, or the prediction market, and is labelled accordingly in the rest of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release timing and cadence
&lt;/h2&gt;

&lt;p&gt;Three timing windows have circulated, and none comes from Alibaba: a September window from a July "roadmap leak" (passed with no launch), an end-of-October claim on r/LocalLLaMA attributed to someone familiar with a launch partner, and an October-to-early-November cadence inference from tracker threads. The &lt;a href="https://manifold.markets/Bayesian/when-will-alibaba-release-qwen-4" rel="noopener noreferrer"&gt;Manifold prediction market&lt;/a&gt; prices a launch before 1 November 2026 at roughly 74%. Read that as weeks, not a date.&lt;/p&gt;

&lt;p&gt;The cadence of the current generation is the concrete precedent. When Qwen3.8 rolled out, the flagship Max launched on 3 August and the 27B open weights appeared on Hugging Face about ten days later. Flash-Next adds a new element: the architecture preview shipped &lt;em&gt;before&lt;/em&gt; the flagship, which suggests Alibaba is decoupling "here is the architecture" from "here is the full model".&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Qwen3.8-Flash-Next&amp;lt;br/&amp;gt;architecture preview&amp;lt;br/&amp;gt;(late Aug 2026)"] --&amp;gt; B["Flagship announcement&amp;lt;br/&amp;gt;(no confirmed date)"]
    B --&amp;gt; C["Open 27B-class weights&amp;lt;br/&amp;gt;~1-2 weeks after&amp;lt;br/&amp;gt;(cadence precedent)"]
    C --&amp;gt; D["Community quants&amp;lt;br/&amp;gt;days after weights"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The practical consequence for local users: the keynote is not the moment weights become available. The Hugging Face drop is the signal to watch, and it has historically lagged the announcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much better could Qwen 4 27B be?
&lt;/h2&gt;

&lt;p&gt;Qwen3.8-27B is a dense vision-language model with strong repository-level coding, terminal automation, and instruction following, under Apache 2.0. Comparing the last three 27B generations on benchmarks where published scores are comparable gives a baseline for what the next one might do.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Qwen3.5-27B&lt;/th&gt;
&lt;th&gt;Qwen3.6-27B&lt;/th&gt;
&lt;th&gt;Qwen3.8-27B&lt;/th&gt;
&lt;th&gt;Avg. gain&lt;/th&gt;
&lt;th&gt;Qwen 4 forecast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench 2.1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;63.4&lt;/td&gt;
&lt;td&gt;73.0&lt;/td&gt;
&lt;td&gt;+9.6*&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76–82&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWE-bench Pro&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;53.5&lt;/td&gt;
&lt;td&gt;61.7&lt;/td&gt;
&lt;td&gt;+8.2*&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65–72&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NL2Repo-Bench&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;36.2&lt;/td&gt;
&lt;td&gt;42.3&lt;/td&gt;
&lt;td&gt;+6.1*&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45–52&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CoWorkBench&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;61.0&lt;/td&gt;
&lt;td&gt;70.7&lt;/td&gt;
&lt;td&gt;+9.7*&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;74–80&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFBench&lt;/td&gt;
&lt;td&gt;76.5&lt;/td&gt;
&lt;td&gt;69.1&lt;/td&gt;
&lt;td&gt;79.5&lt;/td&gt;
&lt;td&gt;+1.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81–86&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPQA Diamond&lt;/td&gt;
&lt;td&gt;85.5&lt;/td&gt;
&lt;td&gt;87.8&lt;/td&gt;
&lt;td&gt;89.2&lt;/td&gt;
&lt;td&gt;+1.9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90–93&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiveCodeBench v6&lt;/td&gt;
&lt;td&gt;80.7&lt;/td&gt;
&lt;td&gt;83.9&lt;/td&gt;
&lt;td&gt;90.3&lt;/td&gt;
&lt;td&gt;+4.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92–95&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Average gain is in percentage points per observed generation-to-generation transition. An asterisk marks benchmarks where only the Qwen3.6-to-Qwen3.8 transition is available; missing Qwen3.5 scores are not assumed to be zero. The Qwen 4 column is a forecast, not a leaked or measured result.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These scores are vendor-reported, and evaluation conditions and harnesses change between generations, so the progression is indicative rather than a controlled experiment. The pattern in it is consistent, though: gains concentrate in agentic coding and long-horizon professional work (Terminal-Bench, SWE-bench Pro, CoWorkBench), while scientific reasoning (GPQA) moves slowly as it approaches saturation.&lt;/p&gt;

&lt;p&gt;A linear extrapolation would overstate the forecast. Benchmark gains get harder as scores rise, some evaluations are near saturation, and Qwen3.8's improvements came from specific training and post-training changes that need not repeat at the same magnitude. The forecast above assumes incremental progress on established reasoning benchmarks and larger gains on coding and agentic reliability — roughly 80 on Terminal-Bench 2.1 and 70 on SWE-bench Pro would already be a meaningful generational step. Worth watching alongside raw scores: whether those scores arrive with fewer reasoning tokens, since in local inference every extra token costs GPU time and memory bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quantization ladder it will land in
&lt;/h2&gt;

&lt;p&gt;Whatever the next 27B's scores are, it will be deployed quantized. The current generation's ladder is the honest floor and ceiling for what a 27B-class model does at each bit level, drawn from cross-source evaluations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;True bits/weight&lt;/th&gt;
&lt;th&gt;Footprint&lt;/th&gt;
&lt;th&gt;Aggregate score&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 (FP16)&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;~54 GB&lt;/td&gt;
&lt;td&gt;85.07 (reference)&lt;/td&gt;
&lt;td&gt;The ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_XL&lt;/td&gt;
&lt;td&gt;~5.2 (not 4)&lt;/td&gt;
&lt;td&gt;17.6 GB&lt;/td&gt;
&lt;td&gt;84.99 (99.9% of FP16)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Default choice&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ternary (Bonsai-class)&lt;/td&gt;
&lt;td&gt;1.76&lt;/td&gt;
&lt;td&gt;5.9 GB&lt;/td&gt;
&lt;td&gt;84.78–86.2 (source-dependent)&lt;/td&gt;
&lt;td&gt;Beats a plain Q2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-bit QAT&lt;/td&gt;
&lt;td&gt;1.125&lt;/td&gt;
&lt;td&gt;3.9 GB&lt;/td&gt;
&lt;td&gt;82.9%&lt;/td&gt;
&lt;td&gt;Phone-class; ~4 points under Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conventional IQ2_XXS&lt;/td&gt;
&lt;td&gt;2.8&lt;/td&gt;
&lt;td&gt;9.4 GB&lt;/td&gt;
&lt;td&gt;72.6–72.7&lt;/td&gt;
&lt;td&gt;The collapse to avoid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two footnotes on that table. The advertised "4-bit" is really about 5.2 bits per weight once metadata and scaling factors are counted, which is why the Q4 row is nearly free relative to BF16. And the aggregate hides where the bits are actually spent: general knowledge, history, coding, and language hold at 97–100% of the FP16 score even at 1-bit, while arithmetic and multilingual tasks are what the bits buy — with the size of that category drop varying noticeably between evaluations.&lt;/p&gt;

&lt;p&gt;This is consistent with the broader picture in &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/efficient-frontier-of-open-models-2026/" rel="noopener noreferrer"&gt;The Efficient Frontier of Open Models in 2026&lt;/a&gt;: the 25–34B class is where open models deliver near-frontier capability per gigabyte, and a Qwen 4 27B that holds the Q4_K_XL position would inherit that slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flash-Next: a working preview of the architecture
&lt;/h2&gt;

&lt;p&gt;Flash-Next is the one piece of Qwen 4 evidence that can be measured today. Against Qwen3.8-27B, on Alibaba's own comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Qwen3.8-27B&lt;/th&gt;
&lt;th&gt;Flash-Next&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSWE 1.1&lt;/td&gt;
&lt;td&gt;42.2&lt;/td&gt;
&lt;td&gt;58.7&lt;/td&gt;
&lt;td&gt;+16.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWE-bench Pro&lt;/td&gt;
&lt;td&gt;61.7&lt;/td&gt;
&lt;td&gt;62.5&lt;/td&gt;
&lt;td&gt;+0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NL2Repo-Bench&lt;/td&gt;
&lt;td&gt;42.3&lt;/td&gt;
&lt;td&gt;48.1&lt;/td&gt;
&lt;td&gt;+5.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CoWorkBench&lt;/td&gt;
&lt;td&gt;70.7&lt;/td&gt;
&lt;td&gt;73.9&lt;/td&gt;
&lt;td&gt;+3.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JobBench&lt;/td&gt;
&lt;td&gt;33.4&lt;/td&gt;
&lt;td&gt;55.7&lt;/td&gt;
&lt;td&gt;+22.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toolathlon Verified&lt;/td&gt;
&lt;td&gt;67.1&lt;/td&gt;
&lt;td&gt;73.5&lt;/td&gt;
&lt;td&gt;+6.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFBench&lt;/td&gt;
&lt;td&gt;79.5&lt;/td&gt;
&lt;td&gt;81.3&lt;/td&gt;
&lt;td&gt;+1.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPQA Diamond&lt;/td&gt;
&lt;td&gt;89.2&lt;/td&gt;
&lt;td&gt;91.7&lt;/td&gt;
&lt;td&gt;+2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiveCodeBench v6&lt;/td&gt;
&lt;td&gt;90.3&lt;/td&gt;
&lt;td&gt;91.7&lt;/td&gt;
&lt;td&gt;+1.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The largest gains are again on agentic workloads — JobBench and DeepSWE — with modest movement on reasoning and competitive programming. One qualification matters when reading this as a Qwen 4 signal: Flash-Next is not a 27B dense model. It is a substantially larger MoE with ~125B main-model parameters, ~6B active, and ~51B additional n-gram embedding parameters, so its results cannot be attributed to architecture alone.&lt;/p&gt;

&lt;p&gt;Three architectural changes in Flash-Next are the substance of the "new-generation architecture" description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen Sparse Attention (QSA)&lt;/strong&gt;, combined with Gated DeltaNet, which keeps a compact running representation of earlier context while retrieving detail from selected regions. Alibaba reports QSA kernel speedups of up to 7.6× in prefill and 4.9× in decoding at one-million-token context — kernel-level numbers, not end-to-end generation speed, but directly relevant to the KV-cache pressure that dominates long-context local inference (see &lt;a href="https://www.glukhov.org/llm-performance/optimization/kv-cache-16gb-long-context/" rel="noopener noreferrer"&gt;KV-cache management for long context on 16 GB&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gated Residual&lt;/strong&gt; connections, which expand the residual stream into four dynamically controlled branches to improve information flow and training stability — invisible to users, but plausibly a contributor to long-horizon behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N-gram embeddings&lt;/strong&gt;, ~51B parameters accessed as lookup operations rather than dense matrix multiplies, which Alibaba suggests can be stored in system RAM and prefetched asynchronously. The parameters still occupy memory somewhere and real gains depend on memory bandwidth, but the design points at more capacity without proportionally more GPU compute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How much of this reaches the smaller Qwen 4 models is unknown. For context on where these designs sit in the wider shift away from monolithic transformers, see &lt;a href="https://www.glukhov.org/ai-models/architectures/what-comes-after-llms/" rel="noopener noreferrer"&gt;What Comes After LLMs?&lt;/a&gt; — hybrid attention and explicit-capacity mechanisms are the same direction the post-transformer research landscape is moving in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model that doesn't exist
&lt;/h2&gt;

&lt;p&gt;One item circulating in Qwen 4 rumor posts should be deleted from any notes it has reached: a &lt;strong&gt;"Qwen 4 Coder 32B-A3B"&lt;/strong&gt; — a laptop-class coding model with a June 2026 release date, Apache 2.0 license, and an 82% SWE-bench Verified score. No such model was ever published through the Qwen blog, GitHub, or Hugging Face organization.&lt;/p&gt;

&lt;p&gt;It even contaminated a benchmark aggregator: LLMCheck ingested the phantom model during a July 2026 data incident and removed it after an August reconciliation against official sources confirmed it does not exist. The 82% figure carries its own internal contradiction — it would have beaten the actual open-weight record at the time (80.2%, held by a 276B-parameter MoE that requires a Mac Studio-class machine), from a claimed 32B laptop model.&lt;/p&gt;

&lt;p&gt;The real shipping coding model in this family is &lt;strong&gt;Qwen3-Coder-Next&lt;/strong&gt;: an MoE with ~80B total and ~3B active parameters, Apache 2.0, at roughly 70.6% SWE-bench Verified. The numbering gap here is the likely origin of the hoax — the version sequence jumped 3.5 → 3.6 → 3.8, and a spec was invented to fill the gap before "4".&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Qwen 4 locally
&lt;/h2&gt;

&lt;p&gt;Assuming a 27B-class dense model with a comparable architecture, the memory targets are familiar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Raw weight memory&lt;/th&gt;
&lt;th&gt;Practical fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;~54 GB&lt;/td&gt;
&lt;td&gt;High-memory workstation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;~27 GB&lt;/td&gt;
&lt;td&gt;32 GB GPU, little headroom left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit&lt;/td&gt;
&lt;td&gt;~13.5 GB&lt;/td&gt;
&lt;td&gt;24 GB GPU with runtime and cache space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-bit&lt;/td&gt;
&lt;td&gt;~10.1 GB&lt;/td&gt;
&lt;td&gt;Marginal 16 GB deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-bit&lt;/td&gt;
&lt;td&gt;~6.8 GB&lt;/td&gt;
&lt;td&gt;Large quality cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For 24–32 GB cards (RTX 3090, 4090, 5090 class), a high-quality 4-bit quantization of a same-size 27B is the expected default, with 32 GB leaving room for context. On 16 GB cards the picture is tighter — a compact quantization fits weights but leaves little for context and runtime buffers, which is where a smaller next-generation model (14B-class) would matter more than an aggressively quantized 27B. Current 16 GB performance references live in &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/best-llm-on-16gb-vram-gpu/" rel="noopener noreferrer"&gt;the 16 GB VRAM benchmark comparison&lt;/a&gt; and in the &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/comparing-qwen-3-6-mtp-vs-standard/" rel="noopener noreferrer"&gt;Qwen 3.6 MTP vs standard decoding runs on an RTX 4080&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;High-memory Apple Silicon is the other profile to watch for the Flash tier: a 64–128 GB unified-memory system can hold useful quantizations of a ~125B/6B-active MoE, and community experiments with SSD-based expert loading and hybrid-memory techniques for Flash-Next already exist — workable, but more complex and not equivalent to fully GPU-resident inference.&lt;/p&gt;

&lt;p&gt;Three practical notes for launch week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runtime support may lag the release.&lt;/strong&gt; New attention and embedding mechanisms usually need fresh kernels; Flash-Next already required specialized implementations. Watch llama.cpp, vLLM, SGLang, MLX, Ollama, and LM Studio — initial support may be limited to development builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not assume a uniform license.&lt;/strong&gt; Qwen3.8-27B is Apache 2.0; Flash-Next ships under the Qwen Community License 1.0. Which license each Qwen 4 checkpoint carries matters for commercial use, redistribution, and fine-tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan hardware against the current generation.&lt;/strong&gt; The ~74% prediction-market figure is a probability, not a commitment. A purchase that needs Qwen 4 to justify itself should be justified against Qwen3.8-27B's ~16–24 GB practical target instead, with Qwen 4 as upside.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A checklist for the next model rumor
&lt;/h2&gt;

&lt;p&gt;The Qwen 4 cycle generalizes. When the next frontier-model rumor lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary source check&lt;/strong&gt; — is it on the lab's blog, GitHub, or Hugging Face organization? A post &lt;em&gt;about&lt;/em&gt; the lab is one hop removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceable dates&lt;/strong&gt; — a date with no named source is a cadence inference. Label it as one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbering-gap suspicion&lt;/strong&gt; — a model whose name fills a gap the lab's own versioning created (3.6 → 3.8 → "so 4?") deserves skepticism, especially with clean specifications attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record-beating specs&lt;/strong&gt; — a rumored benchmark above the actual current record is a red flag, not a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview vs flagship&lt;/strong&gt; — when an architecture preview exists, the flagship announcement is not the moment weights arrive. Plan for the lag.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Qwen 4 is confirmed to exist; almost nothing else about it is confirmed, and the reliable signals are the current generation's numbers, Flash-Next's measured results, and the ten-day weights cadence. The 27B tier will likely remain the single-GPU default for local AI, and the decision worth making now is not when it ships but which memory and quantization target it needs to beat to matter on the hardware already in use.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>qwen</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Tiered Context: A Knowledge Hierarchy for AI Agents</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:21:45 +0000</pubDate>
      <link>https://dev.to/rosgluk/tiered-context-a-knowledge-hierarchy-for-ai-agents-1pc9</link>
      <guid>https://dev.to/rosgluk/tiered-context-a-knowledge-hierarchy-for-ai-agents-1pc9</guid>
      <description>&lt;p&gt;An agent with one flat memory file does not forget things; it cannot stop remembering things it was not asked about. The fix is a tiered context where each knowledge layer enters through a different trigger.&lt;/p&gt;

&lt;p&gt;The failure mode is not forgetting, it is the opposite. In one flat store every fact lives in the same place, enters context the same way, and is therefore available the same way — always. Ask the agent about a client email and it narrates the router configuration while drafting the reply; ask it to troubleshoot a service and it mentions the sponsorship rate quoted last month. The memory pile is a monolith: any change to one concern touches every other concern, and the blast radius of every query is the entire corpus.&lt;/p&gt;

&lt;p&gt;The standard advice is behavioral: teach the agent what is relevant to the current topic. That is the software equivalent of writing "please be careful" on a load-bearing wall — the agent can be instructed, and it will mostly comply, until the day it does not. The structural alternative is knowledge that cannot enter a conversation unless the activity that needs it is already happening. This article defines that structure as a five-layer knowledge hierarchy — what each layer is, what triggers its injection, what belongs in it, and where it fails — and applies it to a concrete multi-domain setup: one general profile, one for a homelab, one for personal matters, one for work, one for education.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming the hierarchy
&lt;/h2&gt;

&lt;p&gt;There is no single industry-standard term, so the definition here is explicit. A &lt;strong&gt;knowledge hierarchy&lt;/strong&gt; is a stratified arrangement of what the agent knows, where each stratum differs in three properties: how it enters the model's context, how large it is allowed to be, and how long it is expected to stay correct. At the knowledge-architecture level, the neighboring paradigms — PKM, RAG, wikis, and memory systems — are compared in &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;, and the runtime contracts assistants actually implement are mapped in &lt;a href="https://www.glukhov.org/ai-systems/memory/memory-systems-in-ai-assistants/" rel="noopener noreferrer"&gt;Memory Systems in AI Assistants&lt;/a&gt;. Both sit inside the &lt;a href="https://www.glukhov.org/knowledge-management/" rel="noopener noreferrer"&gt;Knowledge Management&lt;/a&gt; cluster, which covers the foundations, methods, and tools of human knowledge systems. This article adds the trigger-based classification and the profile boundary that make placement enforceable.&lt;/p&gt;

&lt;p&gt;Two lineages explain the shape of the hierarchy.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;CPU memory hierarchy&lt;/strong&gt;. A processor does not store every byte it might ever need in the same register. It has L1 cache — small, always resident, accessed with zero decision-making — L2, main memory, and disk, each layer larger, slower to reach, and accessed only when the upper layers do not have the answer. The design rule is not "keep everything fast." It is &lt;em&gt;most data rarely matters; pay access cost only when it does&lt;/em&gt;. An agent with one flat memory file is a CPU with everything crammed into L1: the register file is huge, so everything is visible, including the bytes the current instruction did not ask for. &lt;a href="https://arxiv.org/abs/2310.08560" rel="noopener noreferrer"&gt;MemGPT&lt;/a&gt; made the same move for agents, modelling memory as tiers with explicit movement between fast and slow stores.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;cognitive memory taxonomy&lt;/strong&gt;, which divides knowledge into procedural memory (how to do things), semantic memory (facts about the world), and episodic memory (what happened, when, to whom). The taxonomy matters because the failure modes differ by type: procedural knowledge that goes stale is a bug in your workflow, semantic knowledge that goes stale is a wrong fact, and episodic knowledge that leaks is a privacy incident. A hierarchy organized by content type, not by location, tells you which failures are dangerous.&lt;/p&gt;

&lt;p&gt;The organizing axis for this article is neither of the above. Classify knowledge by what causes it to enter context. The injection trigger is the mechanism, and the mechanism is what you can actually enforce. An agent system offers exactly four triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always.&lt;/strong&gt; Injected into every turn, unconditionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task trigger.&lt;/strong&gt; Loaded when the current activity matches a declared condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic recall.&lt;/strong&gt; Surfaced when the current message is similar to something stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit reference.&lt;/strong&gt; Fetched because a task names the file or the query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every layer of the hierarchy is defined by which trigger feeds it. That is the whole design, and the rest of this article is an elaboration of one table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five layers
&lt;/h2&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;Injection trigger&lt;/th&gt;
&lt;th&gt;Typical carrier&lt;/th&gt;
&lt;th&gt;Size discipline&lt;/th&gt;
&lt;th&gt;Fails as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 — Identity&lt;/td&gt;
&lt;td&gt;Who the agent is, how it talks, what it will not do&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;td&gt;SOUL.md or equivalent&lt;/td&gt;
&lt;td&gt;Small; changes rarely&lt;/td&gt;
&lt;td&gt;Bloat — identity becomes a fact store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 — Resident memory&lt;/td&gt;
&lt;td&gt;Durable facts that apply to every session&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;td&gt;MEMORY.md / USER.md or equivalents&lt;/td&gt;
&lt;td&gt;Budgeted in characters; pointer-dense&lt;/td&gt;
&lt;td&gt;Monolith — domain facts migrate in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — Procedural&lt;/td&gt;
&lt;td&gt;How to do recurring tasks, plus where their data lives&lt;/td&gt;
&lt;td&gt;Task trigger&lt;/td&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;Bounded per skill; procedures, not data&lt;/td&gt;
&lt;td&gt;Data creep — skills become databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — Reference&lt;/td&gt;
&lt;td&gt;The system of record: tables, configs, contracts, runbooks&lt;/td&gt;
&lt;td&gt;Explicit reference&lt;/td&gt;
&lt;td&gt;Files, wiki&lt;/td&gt;
&lt;td&gt;Unbounded; greppable, versioned&lt;/td&gt;
&lt;td&gt;Staleness — nobody updates the record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — Retrieved memory&lt;/td&gt;
&lt;td&gt;Episodic and relational facts: decisions, exchanges, state changes&lt;/td&gt;
&lt;td&gt;Semantic recall&lt;/td&gt;
&lt;td&gt;A memory backend with retrieval (e.g. a vector/graph store)&lt;/td&gt;
&lt;td&gt;Budgeted per recall; tag-scorable&lt;/td&gt;
&lt;td&gt;Bleed — recall surfaces the wrong domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    T0["Always"] --&amp;gt; L0["L0 Identity (SOUL.md)"]
    T0 --&amp;gt; L1["L1 Resident memory (MEMORY.md, USER.md)"]
    T1["Task trigger"] --&amp;gt; L2["L2 Procedural (skills)"]
    T2["Explicit reference"] --&amp;gt; L3["L3 Reference (wiki, runbooks)"]
    T3["Semantic recall"] --&amp;gt; L4["L4 Retrieved memory (domain banks)"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Read the table down the trigger column, not across. Layer 0 and Layer 1 are the L1 cache: they pay for their always-resident privilege by being tiny. Layer 2 is instruction cache: pulled on a branch condition, the task matching the skill's trigger. Layer 3 is disk: it can be enormous because nothing enters context until a step of a task explicitly reads a named file. Layer 4 is the awkward one — it is main memory, accessed by similarity, which is the only trigger in the list that does not involve a decision made by you or by the task. Similarity is a probabilistic gate, and that is why it needs the strictest external controls, which the profile section provides. The cabinet image at the top of this article encodes the same idea: the higher a tier sits, the more freely its contents enter a conversation, and the glass partition marks where one domain's shelf ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 0: identity
&lt;/h3&gt;

&lt;p&gt;SOUL.md, or whatever your agent calls its system prompt. Its job is to define &lt;em&gt;who&lt;/em&gt;, not &lt;em&gt;what&lt;/em&gt;: tone, values, refusal boundaries, and memory policy. The memory policy belongs here because it is the only instruction that must be in force before any memory is consulted. An identity file that starts listing server names, client names, or rates has already failed — it has become Layer 1 content wearing Layer 0's badge, and it now rides into every conversation, including ones where it must not appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: resident memory
&lt;/h3&gt;

&lt;p&gt;The persistent memory files: general notes and the user profile. Two rules govern this layer, and they are the rules that separate a working hierarchy from a pile.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;pointers, not facts&lt;/strong&gt;. If a fact serves only one domain, it does not belong here. The correct entry is not the fact itself but a signpost:&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="err"&gt;Infra&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;facts:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wiki/infra/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;memory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bank&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'homelab'.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Clients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;contracts:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wiki/business/clients/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bank&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'business'.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signpost is domain-neutral, so it is safe to carry everywhere. The fact it points at is loaded only by the activity that named the destination.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;a hard budget, enforced&lt;/strong&gt;. If the store has no character limit, it fills, and the filling is always with the most recently discussed topic — so an infra week quietly evicts or surrounds the general context with infra. A budget that is nearly full forces triage, and triage is the only mechanism that keeps this layer honest. The Hermes implementation is a working example: &lt;code&gt;MEMORY.md&lt;/code&gt; capped at 2,200 characters and &lt;code&gt;USER.md&lt;/code&gt; at 1,375, injected as a frozen block at session start, is documented in &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-memory-system/" rel="noopener noreferrer"&gt;Hermes Agent Memory System&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: procedural knowledge
&lt;/h3&gt;

&lt;p&gt;Skills are how the agent encodes &lt;em&gt;how&lt;/em&gt;: the procedure, the pitfalls, and — critically for this article — the &lt;strong&gt;pointers into the deeper layers&lt;/strong&gt;. A skill for homelab administration names which wiki directory holds the inventory and which memory bank to recall. A skill for client work names the contract store and the business bank. The skill's description is its trigger: it is the branch condition that decides whether the procedure, and therefore its pointers, enter context at all.&lt;/p&gt;

&lt;p&gt;This is the layer that makes the hierarchy work operationally, because it is the seam between "what activity is happening" and "what data that activity may see." A task about a client email loads the client skill; the client skill names the business bank and the contracts directory; the homelab bank and the infra directory are simply never referenced from a path the task walked.&lt;/p&gt;

&lt;p&gt;The failure mode is data creep: pasting the actual client list or the actual IP table into the skill body. Then the data is always loaded whenever the procedure is, and the procedure's trigger has become the data's exposure boundary. Skills carry the &lt;em&gt;path to&lt;/em&gt; the data, and nothing else that is sensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: reference data
&lt;/h3&gt;

&lt;p&gt;The wiki, the notes tree, the system of record. This is the layer with the fewest constraints and the fewest surprises, because it is the only layer with a deterministic access path: something enters context only because a step of a task explicitly read that file. Tables, inventories, contracts, runbooks, statistics — the stuff you would keep in a database or a configuration management system. When the corpus is LLM-maintained, the &lt;a href="https://www.glukhov.org/knowledge-management/knowledge-systems-architectures/compiled-knowledge/what-is-llm-wiki/" rel="noopener noreferrer"&gt;LLM Wiki&lt;/a&gt; pattern applies: synthesis happens at ingest time, and the result is stored as structured, linked pages.&lt;/p&gt;

&lt;p&gt;The reason reference data belongs here and not in Layer 4 is that retrieval quality is the wrong tool for tables. Semantic recall is excellent for "what did client X confirm about the deadline" and terrible for "what is the port of the service on host Y" — the latter has exactly one correct row, and a reranker returning the three nearest paragraphs is a liability. Deterministic lookup wins for reference data; similarity wins for episodes. Keep them apart, and each layer's access pattern matches its content shape.&lt;/p&gt;

&lt;p&gt;The failure mode is staleness, which is a maintenance problem, not an architecture problem: the record drifts from reality and the agent, doing everything right, works from a wrong table. The mitigation is the same one any system of record needs — one writer per file, change logs, and an audit cadence — plus the drift-detection and contradiction-check practices covered in &lt;a href="https://www.glukhov.org/knowledge-management/knowledge-systems-architectures/compiled-knowledge/llm-wiki-maintenance-knowledge-drift/" rel="noopener noreferrer"&gt;LLM Wiki Maintenance: Drift, Contradictions and Review&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: retrieved memory
&lt;/h3&gt;

&lt;p&gt;A memory backend with per-turn automatic recall: the agent's episodic and relational memory. Decisions and their dates, "client X confirmed Y," "we tried Z and it did not work." This is where the dynamic, self-organizing part of the system lives — and where the security story gets interesting, because &lt;strong&gt;semantic recall is a probabilistic gate and probabilistic gates bleed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"Billing question for client X" and "which host runs the billing server" are close in embedding space. Any single bank that holds both the client records and the infrastructure inventory will, with non-trivial probability, surface the latter in the former. You can narrow this with tags and recall restrictions, and you should — but a tag filter on a mixed corpus is a discipline, not a structure. The structural answer is to stop mixing: one bank per domain, so that the bleed radius of a recall is, by construction, one domain. Choosing the backend itself — capture policy, dependencies, self-hosting constraints — is a separate comparison, covered in &lt;a href="https://www.glukhov.org/ai-systems/memory/agent-memory-providers/" rel="noopener noreferrer"&gt;Agent Memory Providers Compared&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two mechanical details matter when you wire this layer. Write paths and read paths are separate controls: a bank can be included in recall while being excluded from retention, which is how you build a profile that reads everything and writes to nothing. And every bank should carry its own retention mission — a short statement of what that bank exists to hold. A business bank with the mission "client correspondence, contract terms, negotiation state; never infrastructure details" will extract accordingly, because extraction is an LLM call that reads the mission. &lt;a href="https://developers.openai.com/cookbook/examples/agents_sdk/context_personalization" rel="noopener noreferrer"&gt;OpenAI's personalization cookbook&lt;/a&gt; treats consolidation as the most sensitive and error-prone stage of a memory lifecycle — the retention mission is the contract that keeps that stage honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiles: the boundary that makes separation structural
&lt;/h2&gt;

&lt;p&gt;Up to here, the hierarchy separates knowledge by &lt;em&gt;layer&lt;/em&gt; — necessary but not sufficient. The five layers above are one agent's anatomy. The question this article started with is about &lt;em&gt;domains&lt;/em&gt;: homelab, personal, work, education, and the general context that spans them.&lt;/p&gt;

&lt;p&gt;The mechanism for domain separation is the &lt;strong&gt;profile&lt;/strong&gt; — a full, independent instance of the agent: its own identity file, its own resident memory, its own skills directory, its own scheduled jobs, and, decisively, its own memory-bank wiring. A profile is not a tag on a conversation; it is a separate system that happens to share a machine. When you run &lt;code&gt;agent -p homelab&lt;/code&gt;, you get a process whose entire Layer 0 through Layer 4 is scoped to the homelab domain, and whose recall pool cannot contain the business bank because the business bank is not in its configuration at all.&lt;/p&gt;

&lt;p&gt;This is the difference between the two answers to the leak problem. "Do not disclose infrastructure in business conversations" is a prompt instruction — a probability. "The business profile's recall pool does not include the infrastructure bank" is a configuration — a guarantee. The hierarchy tells you what goes where; the profile boundary is what makes the placement un-bypassable from inside the conversation.&lt;/p&gt;

&lt;p&gt;The worked example, five domains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;common (default)&lt;/th&gt;
&lt;th&gt;homelab&lt;/th&gt;
&lt;th&gt;personal&lt;/th&gt;
&lt;th&gt;work&lt;/th&gt;
&lt;th&gt;education&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity (L0)&lt;/td&gt;
&lt;td&gt;Shared base + memory policy&lt;/td&gt;
&lt;td&gt;Base + "infra operator" frame&lt;/td&gt;
&lt;td&gt;Base + "private, no business tone"&lt;/td&gt;
&lt;td&gt;Base + "professional, client-facing"&lt;/td&gt;
&lt;td&gt;Base + "teacher, assume no prior knowledge"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resident memory (L1)&lt;/td&gt;
&lt;td&gt;Pointers only — to the four domain homes&lt;/td&gt;
&lt;td&gt;Homelab pointers, machine facts&lt;/td&gt;
&lt;td&gt;Personal pointers&lt;/td&gt;
&lt;td&gt;Work pointers&lt;/td&gt;
&lt;td&gt;Course pointers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills (L2)&lt;/td&gt;
&lt;td&gt;General workflow skills&lt;/td&gt;
&lt;td&gt;Infra skills, runbook skill, monitor skill&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Client-operations skill, outreach skill&lt;/td&gt;
&lt;td&gt;Lesson-planning skill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference (L3)&lt;/td&gt;
&lt;td&gt;Shared index of the wiki&lt;/td&gt;
&lt;td&gt;wiki/infra/&lt;/td&gt;
&lt;td&gt;wiki/personal/&lt;/td&gt;
&lt;td&gt;wiki/business/clients/&lt;/td&gt;
&lt;td&gt;wiki/education/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retained bank (L4)&lt;/td&gt;
&lt;td&gt;None (read-only)&lt;/td&gt;
&lt;td&gt;bank: homelab&lt;/td&gt;
&lt;td&gt;bank: personal&lt;/td&gt;
&lt;td&gt;bank: business&lt;/td&gt;
&lt;td&gt;bank: education&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall-only banks (L4)&lt;/td&gt;
&lt;td&gt;homelab, personal, business, education&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two cells in that table do the heavy lifting.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;common profile's bank row is "none, read-only"&lt;/strong&gt;. The general conversation is where you ask cross-cutting questions, so it should be able to see every domain's recalled facts — but it must not write to any of them. Chatter in a general session is exactly the material you do not want extracted into a domain's durable memory, and the read-only wiring removes the choice from the model entirely. If the backend's wiring supports recall-only bank lists (separate from write banks), this is one configuration line per domain bank; if it does not, the common profile recalls from a dedicated general bank and you accept a weaker cross-domain view.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;domain profiles' recall rows are empty&lt;/strong&gt;. Each domain profile sees exactly one bank. The homelab profile will never recall a client's contract term, not because it was told not to, but because the term is not in the pool it queries. This is the bidirectional separation the whole exercise is for: it works symmetrically by construction, with no instruction involved in either direction.&lt;/p&gt;

&lt;p&gt;Three operational consequences follow, and they are why the profile approach costs more than the single-agent approach but is worth it for anyone whose domains are genuinely disjoint.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The trigger becomes the surface.&lt;/strong&gt; If the homelab work happens in a terminal you launch with &lt;code&gt;-p homelab&lt;/code&gt;, and client work happens on a messaging account routed to the work profile, then which hat you are wearing is a property of how you invoked the agent, not a flag you remember to state. Misdirection — accidentally running client tasks in the homelab context — becomes as visible as running the wrong shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled work inherits the boundary.&lt;/strong&gt; Cron jobs and delegated sub-tasks run inside whichever profile owns them. A nightly infra check scheduled in the homelab profile cannot drift business content into the homelab bank, because it never has it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The filesystem is shared, and that is the honest limit.&lt;/strong&gt; Profiles separate &lt;em&gt;context&lt;/em&gt;, not &lt;em&gt;access&lt;/em&gt;. Every profile's terminal can read every profile's files; the boundary protects conversations, not disks. So the layers that hold genuinely sensitive material need their own discipline: credentials in an environment file or a secret store rather than in wiki plaintext, the wiki outside any public repository, and per-directory permissions where the OS helps. Treat the profile boundary as a firewall for context — excellent at its job — and do not let it be mistaken for one for data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bootstrapping from a flat pile
&lt;/h2&gt;

&lt;p&gt;The hierarchy is usually reached from below: an agent that started with one memory file and now keeps everything in it. The migration order matters, because each step is only cheap once the previous one is done.&lt;/p&gt;

&lt;p&gt;First, split the recall pool. Creating one memory bank per domain is a configuration change, not a content rewrite — and it is the step with the highest leverage, because recall bleed stops the day the banks split. Content can migrate afterwards; the boundary should not wait for it.&lt;/p&gt;

&lt;p&gt;Second, budget Layer 1. Set a character cap on the resident files, then audit what is already in them against the cap. Domain-specific facts become pointers to where they will live; what remains is what genuinely applies to every session.&lt;/p&gt;

&lt;p&gt;Third, give the demoted facts a destination. One wiki directory per domain, with the one-writer and change-log discipline, receives what Layer 1 releases. Skills pick up the pointers to it last, because a skill that names a directory that does not exist yet is worse than no skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the hierarchy rots
&lt;/h2&gt;

&lt;p&gt;The architecture is only as good as its maintenance, and each layer rots in a characteristic way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 fills first.&lt;/strong&gt; It is the layer with the most convenient write path ("just remember this"), so it is the first to accumulate domain facts in disguise. The budget makes this visible: when the store is 90% full, audit it for entries that should be pointers. The fix is always the same move — demote facts to Layer 3, promote the signpost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 creeps second.&lt;/strong&gt; Skills are where procedures and data want to live together, because "the procedure references the data constantly" feels reasonable at write time. The check is mechanical: if a skill body contains a table that is the &lt;em&gt;source of truth&lt;/em&gt; for anything, it belongs in the wiki, and the skill should carry a path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4 drifts silently.&lt;/strong&gt; Retrieved memory is append-oriented and self-explanatory, so stale episodes are never corrected, only outlived. The recall preamble ("these memories are historical context, not verified facts") is the standard mitigation. The cleaner mitigation is the same one Layer 3 uses: when an episode becomes a durable fact ("the client signed on this date at these terms"), it gets written into the system of record, where it is visible and editable, and the bank keeps only the narrative. A sharper variant of this drift — the agent's own inference written back as evidence — is worked through in &lt;a href="https://www.glukhov.org/ai-systems/memory/self-reinforcing-memory-loops/" rel="noopener noreferrer"&gt;Self-Reinforcing Memory Loops in AI Agents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profiles multiply.&lt;/strong&gt; Five profiles is manageable; fifteen is a fleet. The discipline is that a new profile appears only when a new &lt;em&gt;domain&lt;/em&gt; appears — not for a new project, a new tool, or a new mood. Project-level separation is what the reference layer's directory structure is for; domain-level separation is what profiles are for. Confusing the two is how people end up with a profile zoo and no clear recall story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway: one placement rule
&lt;/h2&gt;

&lt;p&gt;Put every piece of knowledge at the shallowest layer whose &lt;em&gt;trigger&lt;/em&gt; matches its need: always-needed facts in resident memory as pointers, procedures in skills, reference data in files, episodes in a bank — and put each domain's bank behind a profile that no other domain's profile can recall. The agent then behaves like a well-designed system: the right knowledge is present because the right activity requested it, the wrong knowledge is absent because nothing in the conversation had a path to it, and the only thing that could force the two together is a change in configuration — something you make deliberately, and can therefore see when it happens.&lt;/p&gt;

&lt;p&gt;The flat pile was never a discipline failure. It was an architecture where every fact shares one trigger, and that trigger is "always." Break the trigger apart, and the leaks stop being a question of the model's judgment. They stop being possible.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&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; — the foundational comparison of personal knowledge stores, retrieval pipelines, and agent memory&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/memory/memory-systems-in-ai-assistants/" rel="noopener noreferrer"&gt;Memory Systems in AI Assistants&lt;/a&gt; — the runtime memory stack this hierarchy sits inside&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/knowledge-management/foundations/retrieval-vs-representation/" rel="noopener noreferrer"&gt;Retrieval vs Representation in Knowledge Systems&lt;/a&gt; — why the shape of your store determines what it can answer&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/memory/" rel="noopener noreferrer"&gt;AI Systems Memory hub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>knowledgemanagement</category>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Trending Open Source AI Projects on GitHub, October 2026</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:34:50 +0000</pubDate>
      <link>https://dev.to/rosgluk/trending-open-source-ai-projects-on-github-october-2026-4d42</link>
      <guid>https://dev.to/rosgluk/trending-open-source-ai-projects-on-github-october-2026-4d42</guid>
      <description>&lt;p&gt;GitHub's monthly trending page works for a language, because a language filter already defines the set. Open source AI asks a different question: the work spans five languages, and the useful cut is the job a repository does.&lt;/p&gt;

&lt;p&gt;This snapshot was read from the public GitHub API on 16 October 2026, and inside each group the repositories are ordered by star count. Stars measure attention, and a skills folder can gather that attention faster than a serving engine, so the group comes before the sort. The license column is part of the ranking as well, because several of the most starred projects are fair-code, brand-restricted, or split across two licenses.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.glukhov.org/developer-tools/comparisons/most-popular-rust-projects-on-github/" rel="noopener noreferrer"&gt;Rust&lt;/a&gt; and &lt;a href="https://www.glukhov.org/developer-tools/comparisons/most-popular-go-projects-on-github/" rel="noopener noreferrer"&gt;Go&lt;/a&gt; roundups on this site were built from that same page, with a language filter doing the grouping. Here the eight job groups below replace the filter, and each section states the trade that separates the close options. The closing sections turn the tables into a shortlist you can run, and into a repeatable method for the next snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to rank open source AI projects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Search topics, then assign a job
&lt;/h3&gt;

&lt;p&gt;GitHub topics are the workable substitute for a language filter. Sort a topic such as &lt;code&gt;ai-agents&lt;/code&gt;, &lt;code&gt;llm&lt;/code&gt;, &lt;code&gt;rag&lt;/code&gt;, &lt;code&gt;local-llm&lt;/code&gt;, or &lt;code&gt;agent-memory&lt;/code&gt; by stars, and keep repositories that were pushed in the last few months. Free-text search for "AI" is too wide, and the unfiltered trending page still mixes agents with unrelated tools.&lt;/p&gt;

&lt;p&gt;Three public inputs are enough to rebuild the list. Use topic search for the pool, a scenario catalog such as &lt;a href="https://topopensource.ai/" rel="noopener noreferrer"&gt;Top Open Source AI&lt;/a&gt; for the job labels, and the repository &lt;code&gt;LICENSE&lt;/code&gt; file for the last column. That catalog publishes a composite of its own: stars at 70 percent, contributors at 10 percent, and update recency at 20 percent. This article keeps the simpler rule, so the number in the table is the number on GitHub.&lt;/p&gt;

&lt;p&gt;Useful starting queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/topics/ai-agents?o=desc&amp;amp;s=stars" rel="noopener noreferrer"&gt;ai-agents topic, by stars&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/topics/llm?o=desc&amp;amp;s=stars" rel="noopener noreferrer"&gt;llm topic, by stars&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/topics/rag?o=desc&amp;amp;s=stars" rel="noopener noreferrer"&gt;rag topic, by stars&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/search?q=topic%3Aagent-memory+stars%3A%3E5000&amp;amp;type=repositories&amp;amp;s=stars&amp;amp;o=desc" rel="noopener noreferrer"&gt;agent-memory topic above 5,000 stars&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/trending?since=monthly" rel="noopener noreferrer"&gt;GitHub trending, monthly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Keep software, and set lists aside
&lt;/h3&gt;

&lt;p&gt;A repository earns a row when someone can install it, or when it is a maintained harness that changes how an agent runs. Awesome-lists, prompt directories, and dumps of vendor system prompts collect stars and still leave nothing to deploy. They belong in a reading list, and they stay out of the tables below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a trending column only from your own snapshots
&lt;/h3&gt;

&lt;p&gt;The language roundups could show stars gained in a month because GitHub already computes that delta inside one language. Across these groups, a monthly delta needs two snapshots of your own. Save &lt;code&gt;full_name&lt;/code&gt; and &lt;code&gt;stargazers_count&lt;/code&gt; on the first of the month, then subtract. Until that series exists, total stars are the honest ranking key. "Trending" in the title means actively maintained projects with the attention to match, as of this date.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  topics[Topic search on GitHub]
  catalog[Scenario catalog]
  licenseFile[License file]
  drop[Set lists and prompt dumps aside]
  group[Assign each repo to one job]
  rank[Sort by stars inside the job]
  topics --&amp;gt; drop
  catalog --&amp;gt; group
  drop --&amp;gt; group
  group --&amp;gt; rank
  licenseFile --&amp;gt; rank&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Where the projects sit
&lt;/h2&gt;

&lt;p&gt;These groups are layers you can stack. A coding assistant can call a gateway, a gateway can call Ollama or vLLM, and a memory service can sit beside either of them. Two products from the same layer are usually a second opinion, and they are rarely a better architecture.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  agent[Personal agent]
  coder[Coding assistant]
  skills[Skills and harness]
  memory[Memory]
  docs[RAG and documents]
  flow[Workflow builder]
  gate[Model gateway]
  run[Local runtime]
  coder --&amp;gt; skills
  agent --&amp;gt; memory
  coder --&amp;gt; memory
  agent --&amp;gt; docs
  flow --&amp;gt; docs
  agent --&amp;gt; gate
  coder --&amp;gt; gate
  flow --&amp;gt; gate
  docs --&amp;gt; run
  gate --&amp;gt; run&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Personal AI assistants
&lt;/h2&gt;

&lt;p&gt;This group is for agents that take actions on a machine or across tools, beyond a single coding session. OpenClaw leads by a wide margin. Hermes is the other general agent at this scale, and AutoGPT is the older name that is still accumulating stars while its license has split. A deeper leaderboard for this pair alone, with downloads, token share, and CVE history, is the &lt;a href="https://www.glukhov.org/ai-systems/comparisons/openclaw-hermes-alternatives-popularity/" rel="noopener noreferrer"&gt;OpenClaw vs Hermes Agent comparison&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenClaw&lt;/td&gt;
&lt;td&gt;392,126&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;openclaw/openclaw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes Agent&lt;/td&gt;
&lt;td&gt;254,122&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;NousResearch/hermes-agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl&lt;/td&gt;
&lt;td&gt;191,908&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;td&gt;firecrawl/firecrawl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutoGPT&lt;/td&gt;
&lt;td&gt;187,598&lt;/td&gt;
&lt;td&gt;MIT and Polyform Shield&lt;/td&gt;
&lt;td&gt;Significant-Gravitas/AutoGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Firecrawl is listed here because agents need pages, and it is the web-data project readers actually star. It is a tool, and the assistants call it.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;openclaw/openclaw&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;392,126&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; is a personal agent meant to run on your own operating system and carry out tasks, which is the plain reading of a project that calls itself an AI that does things on any platform. The star count is the largest in this article, and the MIT license is the permissive end of the set. Treat the install as a local automation runtime with broad access to the machine, and scope what it is allowed to touch before you leave it running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hermes Agent
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;NousResearch/hermes-agent&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;254,122&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt;, from Nous Research, is a general agent designed to accumulate skills and context as it is used. It sits in the same job as OpenClaw, with a Python codebase and the same MIT terms. Choose it when you want that growth loop and you are already comfortable operating a Python agent. Running both Hermes and OpenClaw means two permission models to audit.&lt;/p&gt;

&lt;h3&gt;
  
  
  AutoGPT
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Significant-Gravitas/AutoGPT" rel="noopener noreferrer"&gt;Significant-Gravitas/AutoGPT&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;187,598&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT outside autogpt_platform; Polyform Shield inside it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/Significant-Gravitas/AutoGPT" rel="noopener noreferrer"&gt;AutoGPT&lt;/a&gt; is the project that made autonomous agents a mainstream GitHub category, and it is still one of the most starred names in the set. The license is no longer a single grant. Everything outside the &lt;code&gt;autogpt_platform&lt;/code&gt; folder is MIT, and everything inside that folder is under the &lt;a href="https://polyformshield.com/" rel="noopener noreferrer"&gt;Polyform Shield License&lt;/a&gt;, which permits use but reserves the code's commercial value to the producer. Read that split before you build a product on the platform directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firecrawl
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/firecrawl/firecrawl" rel="noopener noreferrer"&gt;firecrawl/firecrawl&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;191,908&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/firecrawl/firecrawl" rel="noopener noreferrer"&gt;Firecrawl&lt;/a&gt; turns sites into data that agents can call. The star count puts it among the assistants even though the job is acquisition, which says a lot about what people are wiring up in 2026. AGPL-3.0 matters if you modify it and offer the result as a service. For a personal agent that only calls a local or vendor endpoint, the license is a smaller constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI coding assistants
&lt;/h2&gt;

&lt;p&gt;Coding agents are the densest part of the 2026 list. OpenCode leads the fully open terminal and editor agents, with Codex and Gemini CLI close behind as the vendor CLIs that happen to be open source. OpenHands, Cline, and Goose are the other programs you can actually run.&lt;/p&gt;

&lt;p&gt;Aider (49,567 stars, Apache-2.0), Continue (36,228, Apache-2.0), and Crush (28,669, FSL-1.1-MIT) are real coding tools in the next band. Crush's license is a &lt;a href="https://fsl.software/" rel="noopener noreferrer"&gt;Functional Source License&lt;/a&gt; that converts to MIT on the schedule written in the file. One coding agent is plenty. A second one is for a comparison week, and after that it is configuration you have to keep in sync.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;213,922&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;anomalyco/opencode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;129,395&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;openai/codex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;107,389&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;google-gemini/gemini-cli&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenHands&lt;/td&gt;
&lt;td&gt;91,004&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;OpenHands/OpenHands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cline&lt;/td&gt;
&lt;td&gt;70,370&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;cline/cline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goose&lt;/td&gt;
&lt;td&gt;55,324&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;aaif-goose/goose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  OpenCode
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/anomalyco/opencode" rel="noopener noreferrer"&gt;anomalyco/opencode&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;213,922&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/anomalyco/opencode" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt; is an open source coding agent with a terminal workflow and a large plugin surface. It has become the default name people cite when they want a coding agent they can inspect, and the MIT license matches that use. The repository is &lt;a href="https://github.com/anomalyco/opencode" rel="noopener noreferrer"&gt;anomalyco/opencode&lt;/a&gt;. Pick it when the agent itself should be something you can patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codex
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;openai/codex&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;129,395&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;Codex&lt;/a&gt; is OpenAI's lightweight coding agent for the terminal, written in Rust and licensed Apache-2.0. It is the open client for a hosted model family, which is a different bargain from a local-first agent. Use it when your team already standardizes on OpenAI models and you want a CLI you can automate. The client being open does not move the model weights onto your disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini CLI
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/google-gemini/gemini-cli" rel="noopener noreferrer"&gt;google-gemini/gemini-cli&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;107,389&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/google-gemini/gemini-cli" rel="noopener noreferrer"&gt;Gemini CLI&lt;/a&gt; brings Gemini into the terminal as an agent, under Apache-2.0. It plays the same role as Codex for a different provider, and the TypeScript codebase is easier to embed in existing Node tooling. Choose it when Gemini is the model you already pay for. Running it beside Codex is reasonable only if each repo has one default agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenHands
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/OpenHands/OpenHands" rel="noopener noreferrer"&gt;OpenHands/OpenHands&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;91,004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/OpenHands/OpenHands" rel="noopener noreferrer"&gt;OpenHands&lt;/a&gt; is a software-development agent with a full task loop, now published from the &lt;code&gt;OpenHands&lt;/code&gt; organization. The MIT license and the higher-level product make it a platform choice, closer to a hosted development environment you can self-host than to a single binary. Use it when you want issues, sandboxes, and a UI around the agent. For a one-person terminal workflow, OpenCode, Codex, or Goose is the smaller install.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cline
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cline/cline" rel="noopener noreferrer"&gt;cline/cline&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;70,370&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/cline/cline" rel="noopener noreferrer"&gt;Cline&lt;/a&gt; is an autonomous coding agent that ships as an SDK, an IDE extension, or a CLI. Apache-2.0 and the editor integration make it the practical pick for people who live in VS Code and still want a library they can call. It overlaps OpenCode more than it overlaps a chat UI. Standardize on one of them per repository so reviews stay readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Goose
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;aaif-goose/goose&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;55,324&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;Goose&lt;/a&gt; is an extensible local agent that installs, edits, runs, and tests code with whatever model you point at it. The repository now lives at &lt;code&gt;aaif-goose/goose&lt;/code&gt;, and the Rust implementation plus Apache-2.0 make it a strong workstation agent. Choose Goose when the model provider should be swappable and the agent should run on the machine that holds the code. It is a quieter star count than OpenCode, and it is a more direct fit for a local model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent skills and harnesses
&lt;/h2&gt;

&lt;p&gt;Topic search will put this group above several real products. Superpowers and ECC are instructions, hooks, and workflows for agents you already run. Spec Kit is GitHub's own process toolkit for spec-driven development. They change agent behavior, and they are still software you compose with a coding assistant from the previous section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Superpowers&lt;/td&gt;
&lt;td&gt;299,886&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;obra/superpowers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECC&lt;/td&gt;
&lt;td&gt;280,382&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;affaan-m/ECC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spec Kit&lt;/td&gt;
&lt;td&gt;141,439&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;github/spec-kit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Superpowers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;obra/superpowers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;299,886&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; is an agentic skills framework and a software-development methodology, distributed mostly as shell and instruction files under MIT. The star count is what you get when a method spreads through coding-agent users in a few months. Install it on top of an agent you already trust, and expect to edit the skills to match the repository. The framework will follow whatever permissions that agent already has.&lt;/p&gt;

&lt;h3&gt;
  
  
  ECC
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/affaan-m/ECC" rel="noopener noreferrer"&gt;affaan-m/ECC&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;280,382&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/affaan-m/ECC" rel="noopener noreferrer"&gt;ECC&lt;/a&gt; is a harness for Claude Code, Codex, OpenCode, Cursor, and similar agents, covering skills, memory, and checks around how the agent works. It is the right shape if you want the same loop on more than one coding agent. It is also another moving layer between you and the model. Adopt it when the team has already picked the agents and wants one shared way to brief them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spec Kit
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;github/spec-kit&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;141,439&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;Spec Kit&lt;/a&gt; is GitHub's toolkit for spec-driven development and the process around it. The maintainer is the platform most of these repositories already live on, and the MIT license is straightforward. Use it when you want a written spec before the agent edits code. It will stay quiet if the team treats the spec as optional paperwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI memory
&lt;/h2&gt;

&lt;p&gt;Memory is the layer that lets an agent resume. The starred projects split into two styles. claude-mem, Beads, and much of Hindsight attach memory to coding sessions. Mem0, Cognee, and Letta are services you call from application agents. Capture policy, dependencies, and self-hosting tradeoffs for these backends are compared in &lt;a href="https://www.glukhov.org/ai-systems/memory/agent-memory-providers/" rel="noopener noreferrer"&gt;Agent Memory Providers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Memvid (16,600 stars, Apache-2.0) and Zep (4,961 stars, Apache-2.0) are smaller options in the same job, Memvid as a single-file store and Zep as an integration-heavy memory API. One memory system per agent is the setup you can still explain later.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;claude-mem&lt;/td&gt;
&lt;td&gt;101,052&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;thedotmack/claude-mem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mem0&lt;/td&gt;
&lt;td&gt;67,243&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;mem0ai/mem0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hindsight&lt;/td&gt;
&lt;td&gt;50,055&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;vectorize-io/hindsight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognee&lt;/td&gt;
&lt;td&gt;32,364&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;topoteretes/cognee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beads&lt;/td&gt;
&lt;td&gt;27,917&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;gastownhall/beads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Letta&lt;/td&gt;
&lt;td&gt;25,156&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;letta-ai/letta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  claude-mem
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;thedotmack/claude-mem&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;101,052&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;claude-mem&lt;/a&gt; records what a coding agent does, compresses that history, and injects the relevant part into later sessions. It talks to Claude Code, OpenClaw, Codex, Gemini CLI, Hermes, Copilot, and OpenCode, which is why it leads this group. Apache-2.0 fits a sidecar you may want to modify. Use it when the thing you keep forgetting is the coding session itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mem0
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/mem0ai/mem0" rel="noopener noreferrer"&gt;mem0ai/mem0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;67,243&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/mem0ai/mem0" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt; is a memory layer for agents and applications, with user, session, and agent state behind an API. It is the default name when the product is a support bot or a personal assistant, and you want that memory outside any one coding tool. Apache-2.0 and a hosted option sit side by side, so self-hosting is a choice you can make later. Prefer it over a coding-session hook when many agents should share the same facts about a user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hindsight
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;vectorize-io/hindsight&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;50,055&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight&lt;/a&gt; is Vectorize's agent memory project, aimed at agents that learn from what they have already done, and it integrates with both coding agents and application frameworks. The MIT license is the most permissive in this group. The project publishes strong benchmark claims, and those claims should be re-run on your own transcripts before you bet a product on them. Pick Hindsight when you want a dedicated memory service and you are willing to compare it with Mem0 on your data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cognee
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;topoteretes/cognee&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;32,364&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt; builds a memory and knowledge graph that agents can query, with an emphasis on running that layer on smaller models. It overlaps Mem0 and it also overlaps the RAG tools below, because a graph of your documents behaves like both memory and retrieval. Apache-2.0 keeps the embedding path replaceable. Choose it when the facts you care about are entities and relationships, and a flat vector index has already felt too lossy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beads
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gastownhall/beads" rel="noopener noreferrer"&gt;gastownhall/beads&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;27,917&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/gastownhall/beads" rel="noopener noreferrer"&gt;Beads&lt;/a&gt; stores coding-agent tasks, plans, and dependencies as files that travel with the git branch. The repository is now &lt;code&gt;gastownhall/beads&lt;/code&gt;, and it is the same project the January Go roundup listed under its earlier path. MIT and a git-backed store make it the memory tool that your existing review process can see. Use it when context should fork with the branch. Use claude-mem or Mem0 when the memory has to outlive a single repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letta
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/letta-ai/letta" rel="noopener noreferrer"&gt;letta-ai/letta&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;25,156&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/letta-ai/letta" rel="noopener noreferrer"&gt;Letta&lt;/a&gt; is the stateful-agent platform that grew out of MemGPT. Memory here is part of the agent runtime, so the agent manages its own context as a feature of the platform. That is a deeper commitment than dropping Mem0 under an agent you already run. Apache-2.0 applies to the open platform. Choose Letta when you want the runtime and the memory designed together, and choose Mem0 when you want memory under agents you do not intend to replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG and knowledge bases
&lt;/h2&gt;

&lt;p&gt;RAG projects turn documents into a context layer. RAGFlow is the end-to-end engine people are starring. LlamaIndex is the document framework, GraphRAG is Microsoft's graph approach, and WeKnora is Tencent's knowledge platform, which also appeared in the January Go roundup and has kept growing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RAGFlow&lt;/td&gt;
&lt;td&gt;92,289&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;infiniflow/ragflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaIndex&lt;/td&gt;
&lt;td&gt;52,513&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;run-llama/llama_index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphRAG&lt;/td&gt;
&lt;td&gt;36,349&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;microsoft/graphrag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WeKnora&lt;/td&gt;
&lt;td&gt;34,210&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Tencent/WeKnora&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  RAGFlow
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/infiniflow/ragflow" rel="noopener noreferrer"&gt;infiniflow/ragflow&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;92,289&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/infiniflow/ragflow" rel="noopener noreferrer"&gt;RAGFlow&lt;/a&gt; is a self-hosted RAG engine that parses messy documents and exposes retrieval to agents, with citations as part of the product. Apache-2.0 and a UI you can hand to a team are why it leads this group by a clear gap. Use it when the job is "put our files behind an answer box" and you want the parser, the index, and the API from one project. Reach for LlamaIndex when you are embedding retrieval inside an application you already control.&lt;/p&gt;

&lt;h3&gt;
  
  
  LlamaIndex
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/run-llama/llama_index" rel="noopener noreferrer"&gt;run-llama/llama_index&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;52,513&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/run-llama/llama_index" rel="noopener noreferrer"&gt;LlamaIndex&lt;/a&gt; is a document-processing framework for agents and RAG pipelines. It is a library with many connectors, and the MIT license fits that role. You assemble the index, the store, and the agent yourself, which is the cost of the flexibility. Choose it for product code. Choose RAGFlow when you want the same job as a service you deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  GraphRAG
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/microsoft/graphrag" rel="noopener noreferrer"&gt;microsoft/graphrag&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;36,349&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/microsoft/graphrag" rel="noopener noreferrer"&gt;GraphRAG&lt;/a&gt; builds a graph over a corpus and answers questions by walking that graph, as Microsoft Research's open implementation of the idea. The technique itself is explained in &lt;a href="https://www.glukhov.org/rag/architecture/advanced-rag-variants-longrag-self-rag-graphrag/" rel="noopener noreferrer"&gt;Advanced RAG: LongRAG, Self-RAG and GraphRAG&lt;/a&gt;. MIT licensing makes it easy to try beside a vector pipeline. Graph construction costs more up front than a chunk-and-embed index, and it pays that cost back on questions that span many documents. Use it when the corpus has structure you are currently forcing into chunks.&lt;/p&gt;

&lt;h3&gt;
  
  
  WeKnora
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Tencent/WeKnora" rel="noopener noreferrer"&gt;Tencent/WeKnora&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;34,210&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/Tencent/WeKnora" rel="noopener noreferrer"&gt;WeKnora&lt;/a&gt; is Tencent's knowledge platform for turning documents into a RAG index, an agent, and a wiki that the project describes as self-maintaining. The project license is MIT, with third-party components under their own terms. GitHub reports that license as Other, because the MIT grant sits under a Tencent preamble. It is the closest cousin to RAGFlow in this table, with a Go core. Prefer it when you want the wiki and the agent in the same box, and compare the parsers on a sample of your own PDFs before you migrate a library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow and agent builders
&lt;/h2&gt;

&lt;p&gt;Builders are where a team draws the flow, or where a developer imports the framework that draws it for them. n8n is the largest by stars and the narrowest by license. Dify and Langflow are the AI-native studios. LangChain is the library underneath a great many of the others.&lt;/p&gt;

&lt;p&gt;CrewAI (59,745 stars, MIT) and AutoGen (61,429 stars, CC-BY-4.0) belong in the same conversation as code-first multi-agent frameworks. AutoGen's Creative Commons attribution license is unusual for a code repository, and it is worth reading before you vendor it. LangChain remains the integration surface those frameworks still meet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;td&gt;207,535&lt;/td&gt;
&lt;td&gt;Sustainable Use License&lt;/td&gt;
&lt;td&gt;n8n-io/n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dify&lt;/td&gt;
&lt;td&gt;158,591&lt;/td&gt;
&lt;td&gt;Modified Apache-2.0&lt;/td&gt;
&lt;td&gt;langgenius/dify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langflow&lt;/td&gt;
&lt;td&gt;155,717&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;langflow-ai/langflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangChain&lt;/td&gt;
&lt;td&gt;147,886&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;langchain-ai/langchain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  n8n
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/n8n-io/n8n" rel="noopener noreferrer"&gt;n8n-io/n8n&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;207,535&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Sustainable Use License&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/n8n-io/n8n" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; is a workflow platform with hundreds of integrations and native AI steps, and it is the project people mean when they want Zapier-style automation they can host. The &lt;a href="https://docs.n8n.io/choose-how-to-use-n8n/" rel="noopener noreferrer"&gt;Sustainable Use License&lt;/a&gt; allows internal business use, personal use, and non-commercial use. Distribution is limited to free, non-commercial copies, and files marked &lt;code&gt;.ee&lt;/code&gt; need a separate enterprise license. It is source-available fair-code, and it is still the right tool for internal automation if those limits match how you run it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dify
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/langgenius/dify" rel="noopener noreferrer"&gt;langgenius/dify&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;158,591&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Modified Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/langgenius/dify" rel="noopener noreferrer"&gt;Dify&lt;/a&gt; is a workspace for agent workflows and RAG pipelines, with model providers and tools in one deployable stack. The license starts from Apache-2.0 and adds conditions: operating a multi-tenant service, meaning multiple workspaces, needs a commercial license, and the console logo and copyright notice stay on the frontend. Single-tenant internal use is the case the extra clauses leave open. Choose Dify when a team needs one UI from prototype to production, and choose Langflow when the license has to stay plain MIT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Langflow
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/langflow-ai/langflow" rel="noopener noreferrer"&gt;langflow-ai/langflow&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;155,717&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/langflow-ai/langflow" rel="noopener noreferrer"&gt;Langflow&lt;/a&gt; is a visual builder for agents and RAG flows, with an MIT license and a star count essentially tied with Dify. The canvas is the product, and the generated flows are something a developer can still own. Use it when you want a diagram of the pipeline and a license a procurement checklist already understands. The trade is a younger application surface than Dify's workspace features.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangChain
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/langchain-ai/langchain" rel="noopener noreferrer"&gt;langchain-ai/langchain&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;147,886&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langchain" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; is the agent-engineering library that most integration docs still assume. MIT and the breadth of connectors keep it in every shortlist, and the abstraction cost is real once the chain is doing something simple. Use it when you are writing application code and you want the adapters already written. Use Dify or Langflow when the deliverable is a workspace, and use a direct provider SDK when the flow is a handful of calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local inference and chat interfaces
&lt;/h2&gt;

&lt;p&gt;This is the layer under everything else. Ollama is the workstation runner, Transformers is the model-definition library, Open WebUI is the chat front end, llama.cpp is the portable C++ engine, and vLLM is the server engine. Hugging Face Transformers is included because so many of the other projects import it, even though you do not "run" it the way you run Ollama. API maturity, hardware support, and production readiness across the runtimes are compared in &lt;a href="https://www.glukhov.org/llm-hosting/comparisons/hosting-llms-ollama-localai-jan-lmstudio-vllm-comparison/" rel="noopener noreferrer"&gt;Ollama vs vLLM vs LM Studio&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;183,334&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;ollama/ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformers&lt;/td&gt;
&lt;td&gt;167,734&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;huggingface/transformers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open WebUI&lt;/td&gt;
&lt;td&gt;155,825&lt;/td&gt;
&lt;td&gt;Open WebUI License&lt;/td&gt;
&lt;td&gt;open-webui/open-webui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;131,311&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;ggml-org/llama.cpp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;93,870&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;vllm-project/vllm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Ollama
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/ollama/ollama" rel="noopener noreferrer"&gt;ollama/ollama&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;183,334&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/ollama/ollama" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; runs current open models locally behind a simple CLI and an OpenAI-compatible API. It was already a leader in the January Go roundup, and at 183,334 stars it is still the default workstation runtime. MIT and a single daemon are the reasons. Use it on a developer machine and on small servers. Move the same weights to vLLM when concurrency, batching, and GPU utilization become the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transformers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/huggingface/transformers" rel="noopener noreferrer"&gt;huggingface/transformers&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;167,734&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/huggingface/transformers" rel="noopener noreferrer"&gt;Transformers&lt;/a&gt; is Hugging Face's library for defining, loading, and running models across text, vision, audio, and multimodal checkpoints. Apache-2.0 and the model hub around it make this the library the rest of the stack imports. You feel it when you are training, converting, or debugging a model. You can ignore it for a long time if Ollama or vLLM already serves the checkpoint you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open WebUI
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/open-webui/open-webui" rel="noopener noreferrer"&gt;open-webui/open-webui&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;155,825&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Open WebUI License&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/open-webui/open-webui" rel="noopener noreferrer"&gt;Open WebUI&lt;/a&gt; is the chat interface most people put in front of Ollama and OpenAI-compatible APIs. The &lt;a href="https://github.com/open-webui/open-webui/blob/main/LICENSE" rel="noopener noreferrer"&gt;Open WebUI License&lt;/a&gt; allows redistribution and then restricts branding. Above 50 end users in any rolling 30 days, you may not alter, remove, or replace the Open WebUI name and marks without written permission or an enterprise license. For a personal or small-team install that keeps the name, it is the polished front end. For a product you will rebrand, budget the enterprise conversation first. Docker deployment and the setup options are walked through in the &lt;a href="https://www.glukhov.org/llm-hosting/llm-frontends/open-webui-overview-quickstart-and-alternatives/" rel="noopener noreferrer"&gt;Open WebUI guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  llama.cpp
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;ggml-org/llama.cpp&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;131,311&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; is the C and C++ inference engine that made local models practical on ordinary hardware. MIT licensing and the GGUF ecosystem are why Ollama and a crowd of smaller apps sit on top of it. Use the project directly when you want the engine, the quantization tools, or a binary with few dependencies. Use Ollama when you want that engine with model management already wrapped.&lt;/p&gt;

&lt;h3&gt;
  
  
  vLLM
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vllm-project/vllm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;93,870&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt; is a high-throughput serving engine for GPUs, with continuous batching and an OpenAI-compatible API. Apache-2.0 and the deployment focus make it the server counterpart to Ollama. Star count is lower because the audience is people who run GPUs, and that audience is the one that feels the difference. Choose vLLM when more than a few concurrent sessions share one machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM gateways
&lt;/h2&gt;

&lt;p&gt;A gateway is the OpenAI-compatible front door in front of many providers. LiteLLM is the common choice, New API is the self-hosted distributor, and Bifrost is the smaller high-throughput gateway. Put a gateway in the stack when you have more than one provider, a budget per key, or a need to fail over. A single local Ollama daemon does not need one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LiteLLM&lt;/td&gt;
&lt;td&gt;61,338&lt;/td&gt;
&lt;td&gt;MIT for the core&lt;/td&gt;
&lt;td&gt;BerriAI/litellm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New API&lt;/td&gt;
&lt;td&gt;50,037&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;td&gt;QuantumNous/new-api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bifrost&lt;/td&gt;
&lt;td&gt;8,837&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;maximhq/bifrost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  LiteLLM
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/BerriAI/litellm" rel="noopener noreferrer"&gt;BerriAI/litellm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;61,338&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT outside enterprise/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/BerriAI/litellm" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt; exposes a large set of provider APIs through one OpenAI-shaped interface, with spend tracking, retries, and guardrails. Code outside the &lt;code&gt;enterprise/&lt;/code&gt; directory is MIT, and that directory has its own license. The Python SDK is what application code imports, and the proxy is what the platform team runs. Use the core when you want one client for many vendors. Check the enterprise directory before you assume the whole repository is MIT.&lt;/p&gt;

&lt;h3&gt;
  
  
  New API
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/QuantumNous/new-api" rel="noopener noreferrer"&gt;QuantumNous/new-api&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;50,037&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/QuantumNous/new-api" rel="noopener noreferrer"&gt;New API&lt;/a&gt; is a model hub that aggregates providers and re-serves them as OpenAI, Claude, or Gemini compatible endpoints. It is the gateway people deploy when they also want user accounts, quotas, and a distribution UI. AGPL-3.0 is the term that matters if you modify the hub and offer it as a service. For an internal gateway with upstream keys, it is a concrete alternative to LiteLLM's proxy, with a Go deployment and a stricter license.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bifrost
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;maximhq/bifrost&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stars&lt;/td&gt;
&lt;td&gt;8,837&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is a Go gateway for models and tools, with failover, budgets, and an MCP gateway in the same process. The star count is far below LiteLLM, and the January Go roundup already had it on the trending list, so the project is younger than it is obscure. Apache-2.0 avoids the enterprise-directory split. Consider it when the gateway is on the request path of a busy service and you want a small Go binary. Keep LiteLLM when you need the long tail of provider adapters today.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stack worth running
&lt;/h2&gt;

&lt;p&gt;The tables are a map, and a workstation still needs a short list. For a local setup in October 2026, Ollama plus one coding agent covers the daily loop. Add Open WebUI if the branding clause fits the user count, Mem0 or claude-mem depending on whether memory belongs to an application or to a coding session, and RAGFlow when the documents are the product. Add LiteLLM or New API only after a second model provider shows up.&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;Default pick&lt;/th&gt;
&lt;th&gt;Choose the alternative when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;vLLM, once several users share a GPU server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat UI&lt;/td&gt;
&lt;td&gt;Open WebUI&lt;/td&gt;
&lt;td&gt;You need to remove the brand above 50 users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding agent&lt;/td&gt;
&lt;td&gt;OpenCode, Codex, Gemini CLI, or Goose&lt;/td&gt;
&lt;td&gt;You self-host the whole task loop: OpenHands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;claude-mem or Mem0&lt;/td&gt;
&lt;td&gt;The facts are a graph: Cognee. The facts follow a git branch: Beads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents&lt;/td&gt;
&lt;td&gt;RAGFlow&lt;/td&gt;
&lt;td&gt;You are embedding retrieval in your own service: LlamaIndex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Builder&lt;/td&gt;
&lt;td&gt;Langflow&lt;/td&gt;
&lt;td&gt;You accept Dify's multi-tenant clause, or n8n's fair-code terms for internal automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway&lt;/td&gt;
&lt;td&gt;LiteLLM core&lt;/td&gt;
&lt;td&gt;You want AGPL and a user-facing hub: New API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Skills packs such as Superpowers and ECC come after the agent is chosen. They are easy to star and easy to accumulate, and three harnesses on one repository will fight over the same instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the ranking current
&lt;/h2&gt;

&lt;p&gt;Repeat the topic queries at the top of this article, assign each new repository to one job, and re-sort that job by stars. Then open the license file. A project that jumped the table on a skills directory, a prompt list, or a weekend of attention can wait for the next snapshot before it replaces a tool you run.&lt;/p&gt;

&lt;p&gt;Store the snapshot. A single CSV of &lt;code&gt;full_name&lt;/code&gt;, &lt;code&gt;stargazers_count&lt;/code&gt;, &lt;code&gt;license&lt;/code&gt;, and &lt;code&gt;pushed_at&lt;/code&gt;, taken monthly, turns the next edition of this page into a real trending column, the same kind of delta the language articles already show. Until then, the groups are the part that ages well, and the star counts are the part that goes stale on purpose.&lt;/p&gt;

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

&lt;p&gt;Star counts, languages, and license identities were read from the GitHub repository API and the repository &lt;code&gt;LICENSE&lt;/code&gt; files on 16 October 2026. Job labels also draw on the public scenario list at Top Open Source AI, which documents its own composite score separately from the sort used here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/topics/ai-agents?o=desc&amp;amp;s=stars" rel="noopener noreferrer"&gt;GitHub topics: ai-agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/topics/llm?o=desc&amp;amp;s=stars" rel="noopener noreferrer"&gt;GitHub topics: llm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/topics/rag?o=desc&amp;amp;s=stars" rel="noopener noreferrer"&gt;GitHub topics: rag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/trending?since=monthly" rel="noopener noreferrer"&gt;GitHub trending, monthly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://topopensource.ai/" rel="noopener noreferrer"&gt;Top Open Source AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://polyformshield.com/" rel="noopener noreferrer"&gt;Polyform Shield License&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fsl.software/" rel="noopener noreferrer"&gt;Functional Source License&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/choose-how-to-use-n8n/" rel="noopener noreferrer"&gt;n8n: Sustainable Use License and plans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/open-webui/open-webui/blob/main/LICENSE" rel="noopener noreferrer"&gt;Open WebUI License file&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>github</category>
      <category>llm</category>
    </item>
    <item>
      <title>Hindsight for Hermes Agent: Self-Hosted Memory with Local LLMs</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 18 Oct 2026 02:50:36 +0000</pubDate>
      <link>https://dev.to/rosgluk/hindsight-for-hermes-agent-self-hosted-memory-with-local-llms-1ke3</link>
      <guid>https://dev.to/rosgluk/hindsight-for-hermes-agent-self-hosted-memory-with-local-llms-1ke3</guid>
      <description>&lt;p&gt;A self-hosted AI agent needs more than a long context window. It needs a way to retain useful facts, retrieve earlier decisions, and revise what it thinks it knows as projects evolve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt; ships with built-in memory files, but an external provider can provide much richer retrieval across sessions. &lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight&lt;/a&gt; is one of the more interesting options in that space. This guide installs it for Hermes with no cloud inference, first as an all-in-one Docker container and then against an existing PostgreSQL server with separate local model endpoints.&lt;/p&gt;

&lt;p&gt;The guide covers deployment, choosing and wiring local embedding, reranking, and generative models, controlling what auto-retain writes into the bank, connecting Hermes, and the operational layer: backup, migration, and day-to-day bank hygiene. The examples use a deliberate port scheme — &lt;code&gt;8080&lt;/code&gt; for the main llama.cpp server, &lt;code&gt;8081&lt;/code&gt; for embeddings, &lt;code&gt;8082&lt;/code&gt; for reranking, &lt;code&gt;11434&lt;/code&gt; for Ollama — so change them to the ports you already use and keep the small servers off the main model's port.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hindsight Does
&lt;/h2&gt;

&lt;p&gt;Hindsight is a persistent agent memory engine with three principal operations: &lt;strong&gt;retain&lt;/strong&gt;, &lt;strong&gt;recall&lt;/strong&gt;, and &lt;strong&gt;reflect&lt;/strong&gt;. Retain extracts and stores information; recall combines semantic, keyword, graph, and temporal retrieval; reflect synthesizes an answer from accumulated memories. Background consolidation can turn overlapping observations into a more useful knowledge layer.&lt;/p&gt;

&lt;p&gt;It is not just an embeddings database. A vector store can locate similar passages, but a memory system also has to preserve chronology, project boundaries, contradictory evidence, and changes in decisions. Those failure modes are explored in &lt;a href="https://www.glukhov.org/ai-systems/memory/self-reinforcing-memory-loops/" rel="noopener noreferrer"&gt;self-reinforcing memory loops&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the wider market, see &lt;a href="https://www.glukhov.org/ai-systems/memory/agent-memory-providers/" rel="noopener noreferrer"&gt;agent memory providers compared&lt;/a&gt;. Hermes' built-in &lt;code&gt;MEMORY.md&lt;/code&gt;, &lt;code&gt;USER.md&lt;/code&gt;, and memory modes are covered in the &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-memory-system/" rel="noopener noreferrer"&gt;Hermes Agent memory system&lt;/a&gt; guide, so this article assumes you know how the built-in layer works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Move Beyond Mnemosyne
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/mnemosyne-memory/" rel="noopener noreferrer"&gt;Mnemosyne for Hermes&lt;/a&gt; offers appealing local-first ideas, including SQLite storage and hybrid retrieval. In practice, installation constraints, version compatibility, write-approval behaviour, and rapid changes around its 4.0 prereleases can make it difficult to operate predictably. Those are operational concerns, not evidence that Mnemosyne's memory model is fundamentally flawed.&lt;/p&gt;

&lt;p&gt;Hindsight is not dependency-free either: its Docker image and local models can consume substantial disk space. Running its API as an independent service, with inference elsewhere, keeps upgrades and troubleshooting isolated from the agent itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a Deployment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;A: All-in-one container&lt;/th&gt;
&lt;th&gt;B: Existing PostgreSQL server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hindsight API and UI&lt;/td&gt;
&lt;td&gt;Docker container&lt;/td&gt;
&lt;td&gt;Docker container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Embedded &lt;code&gt;pg0&lt;/code&gt; in container&lt;/td&gt;
&lt;td&gt;Existing PostgreSQL machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generative LLM&lt;/td&gt;
&lt;td&gt;External llama.cpp&lt;/td&gt;
&lt;td&gt;External llama.cpp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;Default local or external&lt;/td&gt;
&lt;td&gt;External Ollama or llama.cpp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reranking&lt;/td&gt;
&lt;td&gt;Default local or external&lt;/td&gt;
&lt;td&gt;External rerank server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Quick evaluation&lt;/td&gt;
&lt;td&gt;Long-running, isolated installation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here, "all-in-one" means the Hindsight API, UI, and database are in one container; the inference LLM is still hosted elsewhere. This is intentional: Docker's official Hindsight image does not bundle a ready-to-use local generative llama.cpp server.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    H[Hermes Agent] --&amp;gt;|Memory API| A[Hindsight API]
    A --&amp;gt; P[(PostgreSQL)]
    A --&amp;gt; E[Embedding endpoint]
    A --&amp;gt; R[Reranking endpoint]
    A --&amp;gt; L[Generative LLM endpoint]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Use LAN-reachable addresses, not &lt;code&gt;localhost&lt;/code&gt;, for services running on a different host. Restrict these ports to trusted clients; the example &lt;code&gt;dummy&lt;/code&gt; keys are compatibility placeholders, not authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A: All-in-One Hindsight Container
&lt;/h2&gt;

&lt;p&gt;This is the simplest way to evaluate Hindsight without filling the Hermes VM with Python, Torch, and local model dependencies. The container runs Hindsight and embedded PostgreSQL, while your main llama.cpp LLM handles memory extraction and reflection.&lt;/p&gt;

&lt;p&gt;Two image tags matter here. &lt;code&gt;ghcr.io/vectorize-io/hindsight:latest&lt;/code&gt; is the large image — roughly 9 GB on AMD64 — because it bundles a BGE embedder, a MiniLM reranker, and embedded PostgreSQL. &lt;code&gt;latest-slim&lt;/code&gt; is roughly 500 MB and expects those two models to live elsewhere, which is what you want once you point Hindsight at the embedding and reranking setup in Serve and Wire Local Models. Once the health check passes, pin a specific tag: a moving tag is a poor match for a database you intend to keep.&lt;/p&gt;

&lt;p&gt;Create a directory and a Compose file on the Hindsight host:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/services/hindsight
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/services/hindsight
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; compose.yaml &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;YAML&lt;/span&gt;&lt;span class="sh"&gt;'
services:
  hindsight:
    image: ghcr.io/vectorize-io/hindsight:latest
    container_name: hindsight
    restart: unless-stopped
    shm_size: 1gb
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      - "8888:8888"  # Memory API
      - "9999:9999"  # Control-plane UI
    environment:
      HINDSIGHT_API_WORKER_ID: hindsight-standalone
      HINDSIGHT_API_LLM_PROVIDER: openai
      HINDSIGHT_API_LLM_BASE_URL: http://WORKSTATION_IP:8080/v1
      HINDSIGHT_API_LLM_MODEL: LocalMegaLLM
      HINDSIGHT_API_LLM_API_KEY: dummy
      HINDSIGHT_API_LLM_MAX_CONCURRENT: "2"
    volumes:
      - hindsight-pg0:/home/hindsight/.pg0
volumes:
  hindsight-pg0:
&lt;/span&gt;&lt;span class="no"&gt;YAML
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;WORKSTATION_IP&lt;/code&gt; with the host running llama.cpp. On Linux, &lt;code&gt;host.docker.internal&lt;/code&gt; resolves only if the container declares the &lt;code&gt;host-gateway&lt;/code&gt; mapping shown above; without it, a base URL of &lt;code&gt;http://host.docker.internal:8080/v1&lt;/code&gt; points nowhere. The 1 GB shared-memory reservation is particularly useful for embedded PostgreSQL indexing; Docker's small default &lt;code&gt;/dev/shm&lt;/code&gt; can cause failures. A named volume preserves the database when the container is recreated, and a bind mount alternative has to be owned by uid 1000, the user inside the image.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HINDSIGHT_API_WORKER_ID&lt;/code&gt; should stay stable across recreations — Docker's default is the container hostname, and a task that was mid-flight when a container died stays stuck under the old worker id.&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;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
curl &lt;span class="nt"&gt;-f&lt;/span&gt; http://localhost:8888/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your installed Hindsight version exposes health at a different path, inspect &lt;code&gt;http://localhost:8888/docs&lt;/code&gt; and the container logs. The control plane is normally available at &lt;code&gt;http://HINDSIGHT_HOST:9999&lt;/code&gt;. Once the basic deployment works, you can also point its embedding and reranking providers at the configuration in Serve and Wire Local Models rather than loading the default models inside the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not expose ports 8888 or 9999 directly to the internet.&lt;/strong&gt; Use a private network or an authenticated reverse proxy. Embedded PostgreSQL is convenient for testing, but an externally managed database is easier to back up and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: Hindsight Against an Existing PostgreSQL Server
&lt;/h2&gt;

&lt;p&gt;For a long-running installation, I prefer keeping the Hindsight API, database, and inference endpoints separate. The Hindsight VM can remain small because it runs orchestration and HTTP services rather than heavy models.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    H[Hermes VM] --&amp;gt;|port 8888| A[Hindsight VM]
    A --&amp;gt;|port 5432| P[(PostgreSQL host)]
    A --&amp;gt;|port 8080| L[Generative LLM]
    A --&amp;gt;|port 8081 or 11434| E[Embedding endpoint]
    A --&amp;gt;|port 8082| R[Reranking endpoint]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Prepare PostgreSQL and pgvector
&lt;/h3&gt;

&lt;p&gt;Hindsight needs PostgreSQL 14 or newer and a vector extension in the &lt;code&gt;hindsight&lt;/code&gt; database. The extension is per database: installing the package on the server does nothing until &lt;code&gt;CREATE EXTENSION&lt;/code&gt; runs there. On a Debian or Ubuntu PostgreSQL server, install the &lt;code&gt;pgvector&lt;/code&gt; package matching the actual PostgreSQL major version; for example, on PostgreSQL 16:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;postgresql-16-pgvector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package name and availability depend on your configured apt repositories. If it is not available, follow the &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector installation instructions&lt;/a&gt;, or install a compatible package from the PostgreSQL PGDG repository. Do not blindly change the PostgreSQL major version on a shared server.&lt;/p&gt;

&lt;p&gt;Create a dedicated role and database as a PostgreSQL administrator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;hindsight&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'REPLACE_WITH_LONG_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;hindsight&lt;/span&gt; &lt;span class="k"&gt;OWNER&lt;/span&gt; &lt;span class="n"&gt;hindsight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect to the new database and enable the extension:&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; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-d&lt;/span&gt; hindsight &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'CREATE EXTENSION IF NOT EXISTS vector;'&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-d&lt;/span&gt; hindsight &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension normally needs elevated privileges to install; the application role does not need to be a superuser. Hindsight uses pgvector by default and wants 0.8.0 or newer for iterative ANN scans; older servers reject that setting. &lt;code&gt;HINDSIGHT_API_VECTOR_EXTENSION&lt;/code&gt; can alternatively select &lt;code&gt;pgvectorscale&lt;/code&gt; (a DiskANN option Hindsight documents for very large banks) or &lt;code&gt;vchord&lt;/code&gt;, both of which need extra packaging on this exact PostgreSQL build. Switching algorithms after memories exist stops startup and asks for a migration, so choose before the first retain. For a personal Hermes bank, pgvector is the one to install and then stop.&lt;/p&gt;

&lt;p&gt;Allow only the Hindsight host through &lt;code&gt;pg_hba.conf&lt;/code&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;host  hindsight  hindsight  192.168.1.50/32  scram-sha-256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set PostgreSQL &lt;code&gt;listen_addresses&lt;/code&gt; appropriately and use TLS or a trusted private network for cross-host connections. Back up this database independently of the Hindsight Docker container. If the server is shared with other applications, keep Hindsight in its own database, or set &lt;code&gt;HINDSIGHT_API_DATABASE_SCHEMA&lt;/code&gt; to a schema you own; the default schema is &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run Hindsight Against PostgreSQL
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;compose.yaml&lt;/code&gt; on the Hindsight VM. This version deliberately does not start PostgreSQL locally:&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;hindsight&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/vectorize-io/hindsight:latest-slim&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;8888:8888"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9999:9999"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&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;HINDSIGHT_API_LLM_PROVIDER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://WORKSTATION_IP:8080/v1&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LocalMegaLLM&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_LLM_MAX_CONCURRENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_DB_POOL_MIN_SIZE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_DB_POOL_MAX_SIZE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;
      &lt;span class="na"&gt;HINDSIGHT_API_WORKER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hindsight-standalone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the external database URL in a file called &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:YOUR_PASSWORD@POSTGRES_IP:5432/hindsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your password contains &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, or other reserved URL characters, URL-encode it before putting it in the connection string. Test that the Hindsight VM can reach PostgreSQL port 5432, and that &lt;code&gt;pg_hba.conf&lt;/code&gt; allows the Hindsight host — a server bound only to &lt;code&gt;127.0.0.1&lt;/code&gt; refuses the connection even when the URL looks right. Allow the first startup to run database migrations; migrations run on startup unless you set &lt;code&gt;HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=false&lt;/code&gt;. With the API and database up, continue with Choose Models for Hindsight and Serve and Wire Local Models before connecting Hermes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose Models for Hindsight
&lt;/h2&gt;

&lt;p&gt;Deploy puts the API and database on the network. The next decision is which models fill the three inference roles: embeddings for recall candidates, a reranker to re-order them, and a generative LLM for retain, reflect, and consolidation. A practical default for a CPU Hindsight host whose GPU is already busy with conversation is in-process &lt;code&gt;Qwen/Qwen3-Embedding-0.6B&lt;/code&gt;, in-process &lt;code&gt;BAAI/bge-reranker-v2-m3&lt;/code&gt;, and a shared llama.cpp chat model for generative work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding models
&lt;/h3&gt;

&lt;p&gt;Hindsight reads the embedding dimension on startup and sizes the bank to match, so the model you pick at first retain is the model the bank keeps until you rebuild it. Dimensionality and instruction behaviour matter more than leaderboard rank: a stronger 8B embedder is not worth a migration if the bank is already a 384- or 1024-dimensional one. Rows below are sorted from lightest to heaviest; RAM figures are approximate loaded footprint on CPU (FP16 / typical local load), not GGUF file size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Dim&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Loaded&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BAAI/bge-small-en-v1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;33M&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;~100–130 MB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Hindsight default; ships in &lt;code&gt;hindsight:latest&lt;/code&gt;. Fastest local path. Score distribution still calibrates many &lt;code&gt;recall_min_scores&lt;/code&gt; defaults after you leave it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BAAI/bge-base-en-v1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;109M&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;~400 MB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Mid-tier English BGE; locks a 768-dim bank.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~118M&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;~200 MB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Hindsight-documented multilingual MiniLM; 50+ languages at the same 384-dim bank width as BGE-small.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;intfloat/multilingual-e5-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~118M&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;~200 MB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Default for the &lt;code&gt;onnx&lt;/code&gt; provider; light multilingual CPU path when you have an ONNX export.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nomic-ai/nomic-embed-text-v1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~137M&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;~500 MB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Common Ollama embedder; longer context than classic BGE, still English-first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google/embeddinggemma-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;270M&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;~540 MB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;EmbeddingGemma 2 text-only (disable vision/audio towers). 100+ languages + code; MRL truncates to 512/256/128; instruction prefixes for search vs document. Full multimodal load is 740M.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BAAI/bge-m3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;568M&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;~1.1 GB&lt;/td&gt;
&lt;td&gt;CPU or GPU&lt;/td&gt;
&lt;td&gt;Strong multilingual dual-encoder; same width as Qwen3-0.6B, so a same-dim swap still needs a full re-embed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-Embedding-0.6B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6B&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;~1.2 GB&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;This guide's default. Instruction-aware (search instruction on the query side); comfortable on CPU. Local provider auto-applies the instruction; OpenAI-compatible servers need &lt;code&gt;HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-Embedding-4B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;td&gt;2560&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;~8 GB FP16 / ~2.5 GB quant&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;Qwen3 quality tier; locks a 2560-dim bank. Not viable when the conversational GPU is already saturated.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How the model is run is the other half of the choice. Hindsight's embedding providers, from simplest to most moving parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;How it runs&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;In-process SentenceTransformers; no sidecar&lt;/td&gt;
&lt;td&gt;Single Hindsight host, CPU embedder&lt;/td&gt;
&lt;td&gt;Default path. Set &lt;code&gt;HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL&lt;/code&gt;. No llama.cpp, Ollama, or TEI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onnx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;In-process ONNX Runtime&lt;/td&gt;
&lt;td&gt;Lighter CPU than Torch; you already have an ONNX export&lt;/td&gt;
&lt;td&gt;Needs &lt;code&gt;hindsight-api-slim[local-onnx]&lt;/code&gt; and &lt;code&gt;HINDSIGHT_API_EMBEDDINGS_ONNX_*&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openai&lt;/code&gt; / &lt;code&gt;tei&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;External OpenAI-compatible or HF TEI endpoint&lt;/td&gt;
&lt;td&gt;Embeddings off the API process; shared GPU/CPU host&lt;/td&gt;
&lt;td&gt;What Serve and Wire Local Models configures for Ollama / llama.cpp / TEI. Only worth the sidecar if you want isolation or a different machine.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen3-Embedding-0.6B does not need an external embeddings service. On a CPU Hindsight host it loads inside the API process with the &lt;code&gt;local&lt;/code&gt; provider — Hugging Face downloads the weights on first start, and the provider applies the model's search instruction itself:&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="c1"&gt;# Compose environment (or equivalent .env keys)&lt;/span&gt;
&lt;span class="na"&gt;HINDSIGHT_API_EMBEDDINGS_PROVIDER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;span class="na"&gt;HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Qwen/Qwen3-Embedding-0.6B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an external OpenAI-compatible endpoint only when you want the embedder on another machine or off the API process. Ollama is one such host:&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="c1"&gt;# Embeddings: Qwen3-Embedding-0.6B on Ollama&lt;/span&gt;
&lt;span class="na"&gt;HINDSIGHT_API_EMBEDDINGS_PROVIDER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
&lt;span class="na"&gt;HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://192.168.1.123:11434/v1&lt;/span&gt;
&lt;span class="na"&gt;HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen3-embedding:0.6b-q8_0&lt;/span&gt;
&lt;span class="na"&gt;HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the host, port, and tag with whatever &lt;code&gt;ollama list&lt;/code&gt; shows on your inference machine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX&lt;/code&gt; depends on the provider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;&lt;code&gt;HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;local&lt;/code&gt; (&lt;code&gt;Qwen/Qwen3-Embedding-0.6B&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Leave unset / empty — the in-process provider applies Qwen's search instruction itself&lt;/td&gt;
&lt;td&gt;Leave unset / empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External Ollama / llama.cpp / OpenAI-compatible&lt;/td&gt;
&lt;td&gt;Official Qwen3 retrieval instruction (real newline before &lt;code&gt;Query:&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Leave empty — stored memories stay raw text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server already wraps inputs in Qwen's template&lt;/td&gt;
&lt;td&gt;Leave both empty — otherwise the instruction is applied twice&lt;/td&gt;
&lt;td&gt;Leave empty&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the external path, set:&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;# Real newline before Query:. Bash $'...' is the reliable way to pass it.&lt;/span&gt;
&lt;span class="nv"&gt;HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;$'Instruct: Given a web search query, retrieve relevant passages that answer the query&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Query:'&lt;/span&gt;
&lt;span class="nv"&gt;HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Compose YAML the same string needs an explicit &lt;code&gt;\n&lt;/code&gt; (or a literal multiline value). Set the prefixes before the first retain: changing them later means old vectors and new queries no longer sit in the same space.&lt;/p&gt;

&lt;p&gt;Two properties decide more than the leaderboard position. &lt;strong&gt;Instruction sensitivity&lt;/strong&gt;: Qwen3 embeddings apply a search instruction on the query side; without the matching prefix on an OpenAI-compatible server, the stored side and the query side stop sitting in the same space. &lt;strong&gt;Dimension lock&lt;/strong&gt;: because the width is read once at startup, moving from 0.6B to 4B later is a bank migration, not a config change — and even a same-dimension swap (BGE-M3 ↔ Qwen3-0.6B, or EmbeddingGemma 2 ↔ nomic at 768) changes the vector space, so bank vectors are not recomputed automatically. Plan a fresh bank or a manual re-embed/reindex; see Export, import, and migrating the memory bank. After leaving BGE-small, probe &lt;code&gt;recall_min_scores&lt;/code&gt; against the new score distribution — thresholds calibrated on BGE-small can be too tight for Qwen3-0.6B and starve the reranker of candidates. For benchmarks and GGUF/Ollama background on the Qwen3 family, see &lt;a href="https://www.glukhov.org/rag/embeddings/qwen3-embedding-qwen3-reranker-on-ollama/" rel="noopener noreferrer"&gt;Qwen3 Embedding and Reranker models on Ollama&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reranking models
&lt;/h3&gt;

&lt;p&gt;Embeddings recall a candidate set; the reranker scores each query/passage pair and re-orders it before Hindsight injects it. With the &lt;code&gt;local&lt;/code&gt; provider, Hindsight loads a SentenceTransformers cross-encoder (or a generative reranker where supported). With an external Cohere-compatible endpoint, it posts the full Cohere-style body to &lt;code&gt;HINDSIGHT_API_RERANKER_COHERE_BASE_URL&lt;/code&gt;. The reranker is optional, but once the embedder is fixed it is usually the largest recall-quality lever — and, on CPU, often most of the recall wall-clock. Rows are sorted from lightest to heaviest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross-encoder/ms-marco-MiniLM-L-6-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;23M&lt;/td&gt;
&lt;td&gt;Cross-encoder&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Hindsight default; ships in &lt;code&gt;hindsight:latest&lt;/code&gt;. Sub-second for ~20 candidates. Keep as named fallback if a heavier BGE model is too slow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross-encoder/ms-marco-MiniLM-L-12-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~33M&lt;/td&gt;
&lt;td&gt;Cross-encoder&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Same family, higher accuracy; still English-leaning MS MARCO.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross-encoder/mmarco-mMiniLMv2-L12-H384-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~33M&lt;/td&gt;
&lt;td&gt;Cross-encoder&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Hindsight-documented multilingual MiniLM reranker.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BAAI/bge-reranker-base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;278M&lt;/td&gt;
&lt;td&gt;Cross-encoder&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Moderate upgrade over MiniLM; en/zh. Discussed often; skipped when v2-m3 already fits the latency budget.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BAAI/bge-reranker-v2-m3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;568M&lt;/td&gt;
&lt;td&gt;Cross-encoder&lt;/td&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;CPU or GPU&lt;/td&gt;
&lt;td&gt;Practical multilingual default on CPU. On a ~500-memory bank, ~2.6–2.8 s per recall at a 20-candidate floor (~140 ms/candidate; ~93–99% of recall time). Candidate caps 20/40/80; lowering the floor from 20 → 10 roughly halves rerank cost.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-Reranker-0.6B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6B&lt;/td&gt;
&lt;td&gt;Generative (yes-token)&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;GPU preferred&lt;/td&gt;
&lt;td&gt;Instruction-aware, long context. Not a drop-in cross-encoder: needs recent SentenceTransformers generative-reranker support, and llama.cpp &lt;code&gt;/rerank&lt;/code&gt; scoring has known wrong-score bugs (&lt;a href="https://github.com/vectorize-io/hindsight-benchmarks/issues/11" rel="noopener noreferrer"&gt;hindsight-benchmarks#11&lt;/a&gt;). Validate before adopting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BAAI/bge-reranker-v2-gemma&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.5B&lt;/td&gt;
&lt;td&gt;Cross-encoder&lt;/td&gt;
&lt;td&gt;8k&lt;/td&gt;
&lt;td&gt;GPU preferred&lt;/td&gt;
&lt;td&gt;Strong multilingual quality; excessive for a few-hundred-memory personal bank when the GPU is already full.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-Reranker-4B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;td&gt;Generative&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;Higher-quality Qwen candidate; not viable without dedicated VRAM.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-Reranker-8B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B&lt;/td&gt;
&lt;td&gt;Generative&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;Family top tier; same generative-scoring caveats, larger still.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloud API rerankers (Cohere, ZeroEntropy, Alibaba &lt;code&gt;qwen3-rerank&lt;/code&gt;, TypeSafe, and similar) work in Hindsight but send memory text off-box. For a local-only cross-domain layout they are out of scope.&lt;/p&gt;

&lt;p&gt;Two operational constraints dominate the pick. &lt;strong&gt;VRAM&lt;/strong&gt;: if the conversational model already saturates the GPU (for example a Qwen3.8-27B Q5 on a 16 GB card with no headroom), every "GPU preferred" row is off the table and the live choice collapses to MiniLM or BGE-v2-m3 on CPU. &lt;strong&gt;Candidate count&lt;/strong&gt;: &lt;code&gt;HINDSIGHT_API_RERANKER_MAX_CANDIDATES&lt;/code&gt; (30 in the external-endpoint example below) bounds how many passages the model re-scores; each candidate is a full forward pass, so cut the floor before you jump a model tier. For Qwen3 via llama.cpp, the GGUF must keep the ranking head — an old &lt;code&gt;convert_hf_to_gguf.py&lt;/code&gt; that drops it scores every passage near zero — and even a correct conversion still needs a known-good/known-bad passage test until the generative scoring path is confirmed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generative LLM tiers
&lt;/h3&gt;

&lt;p&gt;Embedding and reranking models are not substitutes for the generative LLM used to extract and reason over memories. Hindsight supports a default LLM and operation-specific overrides, which can target the same llama.cpp server or independent endpoints.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hindsight operation&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Practical model tier&lt;/th&gt;
&lt;th&gt;Qwen examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retain&lt;/td&gt;
&lt;td&gt;Extract facts, entities, decisions, and chronology&lt;/td&gt;
&lt;td&gt;4B minimum for tests; 9B preferred&lt;/td&gt;
&lt;td&gt;Qwen3.5-4B / Qwen3.5-9B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall&lt;/td&gt;
&lt;td&gt;Semantic, keyword, graph and temporal retrieval&lt;/td&gt;
&lt;td&gt;No generative LLM required&lt;/td&gt;
&lt;td&gt;Qwen3-Embedding-0.6B + reranker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflect&lt;/td&gt;
&lt;td&gt;Answer questions by synthesizing retrieved memories&lt;/td&gt;
&lt;td&gt;9B to 27B+&lt;/td&gt;
&lt;td&gt;Qwen3.5-9B / Qwen3.8-27B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consolidation&lt;/td&gt;
&lt;td&gt;Reconcile and compress related facts&lt;/td&gt;
&lt;td&gt;9B preferred&lt;/td&gt;
&lt;td&gt;Qwen3.5-9B / Qwen3.8-27B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mental model refresh&lt;/td&gt;
&lt;td&gt;Update longer-lived synthesized understanding&lt;/td&gt;
&lt;td&gt;9B to 35B&lt;/td&gt;
&lt;td&gt;Qwen3.5-9B / Qwen3.6-35B-A3B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are operational recommendations, not official Hindsight minimum parameter counts. A small model may extract a port number correctly but miss that an earlier architectural decision has been superseded. Structured-output reliability and faithful handling of negation matter more than nominal parameter count alone. Hindsight's own temperature defaults are worth leaving alone — &lt;code&gt;0.1&lt;/code&gt; for retain, &lt;code&gt;0.0&lt;/code&gt; for consolidation, &lt;code&gt;0.9&lt;/code&gt; for reflect — and for retain and consolidation, the non-thinking alias is worth serving if your llama.cpp build has one, because the small Qwen3.5 checkpoints spend many tokens in thinking mode.&lt;/p&gt;

&lt;p&gt;For a deployment with an existing Qwen3.8-27B llama.cpp endpoint, start by sharing it across generative Hindsight tasks with low concurrency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HINDSIGHT_API_LLM_PROVIDER=openai
HINDSIGHT_API_LLM_BASE_URL=http://WORKSTATION_IP:8080/v1
HINDSIGHT_API_LLM_MODEL=LocalMegaLLM
HINDSIGHT_API_LLM_API_KEY=dummy
HINDSIGHT_API_LLM_MAX_CONCURRENT=2
HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT=1
HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, route routine extraction to an independent 9B model and reserve the 27B for difficult reflection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HINDSIGHT_API_RETAIN_LLM_PROVIDER=openai
HINDSIGHT_API_RETAIN_LLM_BASE_URL=http://MEMORY_LLM_HOST:8083/v1
HINDSIGHT_API_RETAIN_LLM_MODEL=MemoryLLM
HINDSIGHT_API_RETAIN_LLM_API_KEY=dummy

HINDSIGHT_API_REFLECT_LLM_PROVIDER=openai
HINDSIGHT_API_REFLECT_LLM_BASE_URL=http://WORKSTATION_IP:8080/v1
HINDSIGHT_API_REFLECT_LLM_MODEL=LocalMegaLLM
HINDSIGHT_API_REFLECT_LLM_API_KEY=dummy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The smaller 4B model is economical but needs testing; 9B is a sensible quality/performance compromise. If retain and consolidation share a small model, lower &lt;code&gt;HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt; — the default batch of 8 is a long prompt for a small context window. Qwen3.6-35B-A3B is attractive for a separate well-provisioned inference server (it is a mixture-of-experts model with about 3 billion active parameters, so budget it as a small active model), while a 27B dense model is usually unnecessary for every routine retain event. Limiting background concurrency protects the Hermes conversational model from memory-maintenance bursts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serve and Wire Local Models
&lt;/h2&gt;

&lt;p&gt;If the in-process &lt;code&gt;local&lt;/code&gt; embedding and reranker settings in Choose Models are enough, you can skip the external Ollama and llama.cpp subsections and jump to wiring — or leave Compose on the &lt;code&gt;local&lt;/code&gt; defaults shown there. External endpoints matter when the embedder or reranker should live on another host.&lt;/p&gt;

&lt;h3&gt;
  
  
  External embeddings: Ollama or llama.cpp
&lt;/h3&gt;

&lt;p&gt;External embeddings are for when you want Qwen3-Embedding-0.6B (or another model) on a separate host. Two practical options are Ollama, which handles model acquisition and serving with little configuration, and llama.cpp, which gives precise control over GGUF quantization, CPU threads, device placement, and batching. Model background and benchmarks for both Qwen3 models are in &lt;a href="https://www.glukhov.org/rag/embeddings/qwen3-embedding-qwen3-reranker-on-ollama/" rel="noopener noreferrer"&gt;Qwen3 Embedding and Reranker models on Ollama&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Qwen3-Embedding-0.6B returns 1024-dimensional vectors, and Hindsight reads that width on startup and sizes the database to match. A bank created with the bundled 384-dimensional BGE model cannot take this model — start from an empty database or volume, and decide the embedding model before the first retain. An embeddings provider switch later requires re-embedding stored records even if both models use the same dimensionality.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 1: Ollama for Qwen3-Embedding-0.6B
&lt;/h4&gt;

&lt;p&gt;Install Ollama on the inference host using its &lt;a href="https://ollama.com/download/linux" rel="noopener noreferrer"&gt;official Linux installation documentation&lt;/a&gt;, then start the embedding model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen3-embedding:0.6b
ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For remote access, configure Ollama's &lt;code&gt;OLLAMA_HOST=0.0.0.0:11434&lt;/code&gt; in its service environment, restart the service, and firewall it to the Hindsight VM — the procedure is covered in &lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-remote-access/" rel="noopener noreferrer"&gt;Ollama remote access&lt;/a&gt;. A daemon bound only to &lt;code&gt;127.0.0.1&lt;/code&gt; is invisible to the Hindsight container. First check the model name with &lt;code&gt;ollama list&lt;/code&gt;, and test the standard embedding 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://EMBED_HOST:11434/api/embed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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":"qwen3-embedding:0.6b","input":"A test memory"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hindsight's OpenAI embeddings provider instead calls Ollama's OpenAI-compatible &lt;code&gt;/v1/embeddings&lt;/code&gt; endpoint. Verify that endpoint works with your Ollama version before configuring Hindsight. &lt;strong&gt;Ollama is straightforward for embeddings; do not assume its API offers a compatible reranking endpoint.&lt;/strong&gt; For reranking, prefer the in-process &lt;code&gt;local&lt;/code&gt; BGE path below, or a separate llama.cpp / TEI endpoint after you have validated scores.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 2: llama.cpp for Qwen3-Embedding-0.6B
&lt;/h4&gt;

&lt;p&gt;Download a verified &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding-0.6B GGUF&lt;/a&gt; conversion. Use the GGUF filename you actually downloaded:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# start-embedding.sh - CPU-only embedding server&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/prj"&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; ./llama.cpp/llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8081 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/ggufs/Qwen3-Embedding-0.6B-Q8_0.gguf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alias&lt;/span&gt; qwen3-embedding-0.6b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--embedding&lt;/span&gt; &lt;span class="nt"&gt;--pooling&lt;/span&gt; last &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ctx-size&lt;/span&gt; 4096 &lt;span class="nt"&gt;--parallel&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--threads&lt;/span&gt; 4 &lt;span class="nt"&gt;--threads-batch&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 512 &lt;span class="nt"&gt;--ubatch-size&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--n-gpu-layers&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qwen3 embeddings use last-token pooling, unlike the CLS pooling used by some BGE models. A CPU-only llama.cpp build is preferable if you want absolutely no CUDA initialization on a workstation whose GPU is occupied by a large conversational model. Do not enable &lt;code&gt;--reranking&lt;/code&gt; on this embedding server: that combination is a known way to get a vector of zeros.&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://EMBED_HOST:8081/v1/embeddings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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":"qwen3-embedding-0.6b","input":"A test memory"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.data[0].embedding | length'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full-dimension Qwen3-Embedding-0.6B output should contain 1024 floats. Check that the GGUF conversion and your Hindsight query/document instruction handling are compatible; instruction-sensitive embedding models can lose accuracy when prompted incorrectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reranker: local BGE or Qwen3 via llama.cpp
&lt;/h3&gt;

&lt;p&gt;Reranking is different from embeddings: the model scores each query/document pair and returns a relevance score. On a CPU-only Hindsight host — or whenever the conversational GPU has no VRAM headroom — the practical path is Hindsight's &lt;code&gt;local&lt;/code&gt; provider with &lt;code&gt;BAAI/bge-reranker-v2-m3&lt;/code&gt; (or the lighter MiniLM default). No sidecar is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HINDSIGHT_API_RERANKER_PROVIDER=local
HINDSIGHT_API_RERANKER_LOCAL_MODEL=BAAI/bge-reranker-v2-m3
HINDSIGHT_API_RERANKER_MAX_CANDIDATES=20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qwen3-Reranker-0.6B is a generative yes-token scorer, not a classic cross-encoder. It can be served through llama.cpp with a GGUF conversion designed for ranking, but treat that path as experimental until you confirm scores on a known-good/known-bad pair: an old &lt;code&gt;convert_hf_to_gguf.py&lt;/code&gt; that drops the ranking head scores every passage near zero, and llama.cpp's native &lt;code&gt;/rerank&lt;/code&gt; path has known wrong-score reports for this family.&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# start-reranker.sh - GPU or CPU as selected by --n-gpu-layers&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/prj"&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; ./llama.cpp/llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8082 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/ggufs/Qwen3-Reranker-0.6B-Q8_0.gguf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alias&lt;/span&gt; qwen3-reranker-0.6b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--embedding&lt;/span&gt; &lt;span class="nt"&gt;--reranking&lt;/span&gt; &lt;span class="nt"&gt;--pooling&lt;/span&gt; rank &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ctx-size&lt;/span&gt; 4096 &lt;span class="nt"&gt;--parallel&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 512 &lt;span class="nt"&gt;--ubatch-size&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--n-gpu-layers&lt;/span&gt; 99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;--n-gpu-layers 0&lt;/code&gt; to run it on CPU. The GPU version needs additional VRAM for compute buffers and context, not just model weights; avoid assuming that the file size is the total GPU allocation.&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://RERANK_HOST:8082/v1/rerank &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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":"qwen3-reranker-0.6b", "query":"PostgreSQL storage", "documents":["Hindsight stores memories in PostgreSQL.","A recipe for bread."], "top_n":2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PostgreSQL document should rank above the unrelated document; all-zero relevance scores mean the GGUF is the wrong conversion, and recall will look random even when the facts are stored. Check the exact &lt;code&gt;results[].index&lt;/code&gt; and &lt;code&gt;results[].relevance_score&lt;/code&gt; shape: Hindsight expects a Cohere-compatible rerank response, and llama.cpp compatibility can vary across builds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wire endpoints into Hindsight
&lt;/h3&gt;

&lt;p&gt;Add the following environment settings to the Hindsight Compose service, or store them in its &lt;code&gt;.env&lt;/code&gt; file. Pick one embeddings path — in-process &lt;code&gt;local&lt;/code&gt;, Ollama, or llama.cpp — never two 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;# Option A: in-process Qwen3 on the Hindsight host (CPU; no sidecar)
HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=Qwen/Qwen3-Embedding-0.6B

# Option B: llama.cpp embedding service
# HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
# HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=http://EMBED_HOST:8081/v1
# HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=qwen3-embedding-0.6b
# HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=dummy
# HINDSIGHT_API_EMBEDDINGS_OPENAI_BATCH_SIZE=8
# HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX=$'Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery:'
# HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX=

# Option C: Ollama on another host
# HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
# HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=http://192.168.1.40:11434/v1
# HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=qwen3-embedding:0.6b-q8_0
# HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=dummy
# HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX=$'Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery:'
# HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX=

# Option: local BGE reranker (no sidecar; preferred on CPU)
HINDSIGHT_API_RERANKER_PROVIDER=local
HINDSIGHT_API_RERANKER_LOCAL_MODEL=BAAI/bge-reranker-v2-m3
HINDSIGHT_API_RERANKER_MAX_CANDIDATES=20

# Option: llama.cpp Cohere-compatible rerank endpoint (validate Qwen3 scores first)
# HINDSIGHT_API_RERANKER_PROVIDER=cohere
# HINDSIGHT_API_RERANKER_COHERE_BASE_URL=http://RERANK_HOST:8082/v1/rerank
# HINDSIGHT_API_RERANKER_COHERE_MODEL=qwen3-reranker-0.6b
# HINDSIGHT_API_RERANKER_COHERE_API_KEY=dummy
# HINDSIGHT_API_RERANKER_MAX_CANDIDATES=30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The variable names are provider-specific, and a near-miss is silent: &lt;code&gt;HINDSIGHT_API_EMBEDDINGS_BASE_URL&lt;/code&gt; is the wrong key, and Hindsight quietly aims at OpenAI's own embedding endpoint instead of your local server. Use &lt;code&gt;HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL&lt;/code&gt;. With the Cohere-compatible reranker path, Hindsight posts the full Cohere-style body to &lt;code&gt;HINDSIGHT_API_RERANKER_COHERE_BASE_URL&lt;/code&gt;; if the container log shows a 404 for &lt;code&gt;/v1/rerank/rerank&lt;/code&gt;, your build appends &lt;code&gt;/rerank&lt;/code&gt; on its own and the value should stop at &lt;code&gt;/v1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For Options B and C, use the &lt;code&gt;HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX&lt;/code&gt; value from Embedding models (Option A leaves it unset). Test query-to-passage retrieval on your actual memories after the first retain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control What Gets Remembered
&lt;/h2&gt;

&lt;p&gt;With the stack running, the next failure mode is usually the write path: Hermes defaults to &lt;code&gt;auto_retain: true&lt;/code&gt;, so every conversation turn can be extracted into the bank. Incorrect guesses, discarded plans, and exploratory statements become searchable facts, and later recall treats them as evidence — the loop described in &lt;a href="https://www.glukhov.org/ai-systems/memory/self-reinforcing-memory-loops/" rel="noopener noreferrer"&gt;self-reinforcing memory loops&lt;/a&gt;. Capture policy for Hindsight among other providers is summarized in &lt;a href="https://www.glukhov.org/ai-systems/memory/agent-memory-providers/" rel="noopener noreferrer"&gt;agent memory providers compared&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capture modes that exist today
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Hermes / Hindsight settings&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Continuous&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auto_retain: true&lt;/code&gt;, &lt;code&gt;retain_every_n_turns: 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Every turn is retained and extracted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batched auto&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auto_retain: true&lt;/code&gt;, raise &lt;code&gt;retain_every_n_turns&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fewer, larger retain payloads — still automatic, not a review queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit-only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auto_retain: false&lt;/code&gt; + tool &lt;code&gt;hindsight_retain&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Storage only when the agent calls the tool (for example after you say "remember this")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session synthesis&lt;/td&gt;
&lt;td&gt;End-of-session &lt;code&gt;hindsight_retain&lt;/code&gt; of a summary, or bank reflect / consolidation&lt;/td&gt;
&lt;td&gt;A curated write if you or the agent discipline the call — not a built-in flush-for-approval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Setting &lt;code&gt;auto_retain&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; disables automatic storage. It does &lt;strong&gt;not&lt;/strong&gt; add a human approval dialog before each write. The agent can still call &lt;code&gt;hindsight_retain&lt;/code&gt; whenever its tools allow; treat explicit retain as deliberate capture, not a pending queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  The approval-queue gap
&lt;/h3&gt;

&lt;p&gt;A common wish is a pending queue of candidate memories that you review and approve before they enter the bank. Hindsight under Hermes does not provide that contract today. &lt;a href="https://www.glukhov.org/ai-systems/hermes/mnemosyne-memory/" rel="noopener noreferrer"&gt;Mnemosyne for Hermes&lt;/a&gt; implements provider-specific staged writes in some versions; that path is not a shared Hermes standard and does not apply to the Hindsight plugin. Until an upstream approval flow exists, the workable pattern is &lt;code&gt;auto_retain: false&lt;/code&gt;, deliberate &lt;code&gt;hindsight_retain&lt;/code&gt;, and inspection or deletion in the Hindsight control-plane UI before the next session relies on a bad fact. There is no missing &lt;code&gt;write_approval&lt;/code&gt; key for Hindsight to hunt for in &lt;code&gt;config.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bank steering (missions, labels, directives, mental models)
&lt;/h3&gt;

&lt;p&gt;People often call the next layer "finetuning." In Hindsight it is mostly &lt;strong&gt;bank configuration&lt;/strong&gt;, not Hermes &lt;code&gt;config.json&lt;/code&gt; alone: missions and extraction settings steer what retain keeps; labels structure entities; directives and mental models shape later synthesis.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Where it lives&lt;/th&gt;
&lt;th&gt;How it helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retain mission (&lt;code&gt;retain_mission&lt;/code&gt; / Hermes &lt;code&gt;bank_retain_mission&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Bank config&lt;/td&gt;
&lt;td&gt;Plain-language "pay attention to X, ignore Y" injected into extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extraction mode (&lt;code&gt;concise&lt;/code&gt;, &lt;code&gt;verbose&lt;/code&gt;, &lt;code&gt;custom&lt;/code&gt;, …)&lt;/td&gt;
&lt;td&gt;Bank config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;concise&lt;/code&gt; (default) keeps long-term-worthy facts; &lt;code&gt;custom&lt;/code&gt; replaces built-in rules via &lt;code&gt;retain_custom_instructions&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity labels&lt;/td&gt;
&lt;td&gt;Bank config&lt;/td&gt;
&lt;td&gt;Controlled &lt;code&gt;key:value&lt;/code&gt; vocabulary at retain time → graph links and optional tags; improves retrieval structure, does not by itself reject wrong statements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directives ("hard rules")&lt;/td&gt;
&lt;td&gt;Bank directives API&lt;/td&gt;
&lt;td&gt;Explicit instructions enforced during &lt;strong&gt;reflect&lt;/strong&gt; (tag-scoped); constrain synthesis, not a write-approval gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mental models&lt;/td&gt;
&lt;td&gt;Bank mental-model API&lt;/td&gt;
&lt;td&gt;Standing answers to recurring questions, refreshed via reflect from matching memories; derived, so bad source facts still poison them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflect / observations mission&lt;/td&gt;
&lt;td&gt;Bank config&lt;/td&gt;
&lt;td&gt;Steers synthesis and consolidation, not the raw retain firehose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Missions, concise extraction, and labels reduce noise and improve retrieval. They do &lt;strong&gt;not&lt;/strong&gt; replace turning off auto-retain when accuracy matters more than completeness. Prefer a short retain mission that names durable project decisions and ignores chat logistics; add entity labels only when you have a real taxonomy; add mental models for two or three recurring questions after the bank already holds clean facts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended hygiene profile
&lt;/h3&gt;

&lt;p&gt;A concrete starting point for a personal Hermes bank:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local_external"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"api_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://HINDSIGHT_VM_IP:8888"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bank_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hermes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memory_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hybrid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auto_recall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recall_sync"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auto_retain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retain_async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recall_prefetch_method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recall"&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;On the bank, set a retain mission and leave extraction in &lt;code&gt;concise&lt;/code&gt; mode (via the control plane or Banks API). After an explicit retain, open the UI, confirm the observation, and delete mistakes before consolidation or mental-model refresh runs. Leave &lt;code&gt;recall_prefetch_method&lt;/code&gt; at &lt;code&gt;recall&lt;/code&gt; — &lt;code&gt;reflect&lt;/code&gt; spends a chat-model call before every turn and injects synthesis rather than underlying facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect Hermes Agent to Hindsight
&lt;/h2&gt;

&lt;p&gt;On the &lt;strong&gt;Hermes VM&lt;/strong&gt;, install the catalog plugin, then run the setup wizard. Select Hindsight and the &lt;code&gt;local_external&lt;/code&gt; mode; the plugin does not need to start embedded PostgreSQL or run the Hindsight model stack locally, and &lt;code&gt;local_embedded&lt;/code&gt; would start a second Hindsight daemon beside the container you just ran.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes plugins &lt;span class="nb"&gt;install &lt;/span&gt;hindsight
hermes memory setup hindsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There will be Interesting set of options of agent role using hindsight&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes memory status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The external Hindsight connection lives in &lt;code&gt;~/.hermes/hindsight/config.json&lt;/code&gt;. An illustrative configuration that keeps continuous capture (Hermes defaults) is:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local_external"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"api_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://HINDSIGHT_VM_IP:8888"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bank_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hermes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memory_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hybrid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auto_recall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recall_sync"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auto_retain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retain_async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retain_every_n_turns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;For explicit-only retain and bank steering, use the hygiene profile in Control What Gets Remembered instead of leaving &lt;code&gt;auto_retain&lt;/code&gt; on. &lt;code&gt;recall_sync: false&lt;/code&gt; prioritizes interactive responsiveness but may inject recalled context on the &lt;strong&gt;following&lt;/strong&gt; turn. Use synchronous recall when the current request must incorporate the latest memories before the model starts answering. &lt;code&gt;retain_async: true&lt;/code&gt; avoids blocking ordinary responses while memories are processed.&lt;/p&gt;

&lt;p&gt;The plugin's recall default is observations only; raw world and experience facts stay in the bank and out of the prompt until consolidation has written an observation, or until you widen &lt;code&gt;recall_types&lt;/code&gt; to &lt;code&gt;observation,world,experience&lt;/code&gt;. Widening it for the first week and watching the control plane, then narrowing, is a reasonable order.&lt;/p&gt;

&lt;p&gt;Built-in Hermes memory stays active next to the provider; keeping &lt;code&gt;MEMORY.md&lt;/code&gt; and &lt;code&gt;USER.md&lt;/code&gt; on while you learn what Hindsight adds is fine, and the memory-system article is the place to decide whether the flat files should stay.&lt;/p&gt;

&lt;p&gt;Restart the gateway if it is already running:&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 restart
hermes memory status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status should identify Hindsight as the active provider. For a wider command reference, including Hermes' TUI and plugin commands, see the &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-cli-cheatsheet/" rel="noopener noreferrer"&gt;Hermes Agent CLI cheatsheet&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate Memory, Not Just Installation
&lt;/h3&gt;

&lt;p&gt;Ask Hermes to remember a disposable fact about a test project, start another session, and ask for that fact again. Retention is asynchronous by default and extraction waits on the chat model, so give the main llama.cpp server a moment before judging. Then test a harder case: tell Hermes that an earlier decision has changed and ask which decision is current and why. Check Hindsight's UI and stored observations rather than assuming &lt;code&gt;memory status&lt;/code&gt; proves ingestion and recall are correct.&lt;/p&gt;

&lt;p&gt;If the answer comes back empty, inspect the control plane before changing models. A retain can succeed and still be invisible to recall while &lt;code&gt;recall_types&lt;/code&gt; is observations only and consolidation has not run — widening &lt;code&gt;recall_types&lt;/code&gt; is the first check. A connection refused in the embedding server log means the container cannot reach the embedding port, and all-zero reranker scores mean the GGUF is the wrong conversion.&lt;/p&gt;

&lt;p&gt;Write-path choices (&lt;code&gt;auto_retain&lt;/code&gt;, missions, approval gap) are covered in Control What Gets Remembered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting and Operational Notes
&lt;/h2&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;First check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hermes says provider unavailable&lt;/td&gt;
&lt;td&gt;Wrong &lt;code&gt;local_external&lt;/code&gt; URL or network rule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl http://HINDSIGHT_VM_IP:8888/health&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker starts, PostgreSQL errors&lt;/td&gt;
&lt;td&gt;Missing extension, wrong credentials, or insufficient shared memory&lt;/td&gt;
&lt;td&gt;Container logs and &lt;code&gt;pg_extension&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings return 401 or 404&lt;/td&gt;
&lt;td&gt;Incorrect OpenAI-compatible URL, wrong env key name, or model alias mismatch&lt;/td&gt;
&lt;td&gt;Direct &lt;code&gt;/v1/embeddings&lt;/code&gt; test and variable names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reranking fails&lt;/td&gt;
&lt;td&gt;Endpoint or response format mismatch, including doubled &lt;code&gt;/rerank&lt;/code&gt; path&lt;/td&gt;
&lt;td&gt;Direct &lt;code&gt;/v1/rerank&lt;/code&gt; test and container log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall returns nothing usable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;recall_types&lt;/code&gt; limited to observations, or zero reranker scores&lt;/td&gt;
&lt;td&gt;Control plane, then &lt;code&gt;recall_types&lt;/code&gt;, then reranker conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing bank reports dimension mismatch&lt;/td&gt;
&lt;td&gt;Embedding model changed&lt;/td&gt;
&lt;td&gt;Rebuild vectors/reimport with a consistent model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes responses slow down&lt;/td&gt;
&lt;td&gt;Synchronous recall or shared LLM contention&lt;/td&gt;
&lt;td&gt;Turn off sync recall; cap retain concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Irrelevant memories repeat&lt;/td&gt;
&lt;td&gt;Weak extraction, noisy retrieval, or insufficient consolidation&lt;/td&gt;
&lt;td&gt;Inspect observations and relevance scores&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A practical small deployment is 2-4 vCPUs and 4-6 GB RAM for the Hindsight API VM &lt;strong&gt;when PostgreSQL and all inference are external&lt;/strong&gt;. Disk allocation still depends on the chosen Docker image and retained image layers; allocate roughly 30-40 GB initially with room to expand. Size the PostgreSQL host based on actual memory volume, storage performance, and the other databases it serves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back up and restore the Hindsight bank
&lt;/h2&gt;

&lt;p&gt;The memory bank is the only state that matters, and it lives in PostgreSQL. In Option B that is a database you administer; in Option A it is the embedded &lt;code&gt;pg0&lt;/code&gt; named volume inside the container. Back it up independently of the Hindsight container — recreating the container without first capturing the volume or the database discards the bank, and &lt;code&gt;hermes backup&lt;/code&gt; does not reach a bank running in a separate service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — external PostgreSQL.&lt;/strong&gt; Dump the &lt;code&gt;hindsight&lt;/code&gt; database in custom format, then restore it into a scratch database to verify the archive before you trust it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_dump &lt;span class="nt"&gt;-h&lt;/span&gt; POSTGRES_IP &lt;span class="nt"&gt;-U&lt;/span&gt; hindsight &lt;span class="nt"&gt;-Fc&lt;/span&gt; hindsight &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hindsight-bank.dump

&lt;span class="c"&gt;# restore into a scratch database to prove the dump is usable&lt;/span&gt;
createdb &lt;span class="nt"&gt;-h&lt;/span&gt; POSTGRES_IP &lt;span class="nt"&gt;-U&lt;/span&gt; postgres hindsight-verify
pg_restore &lt;span class="nt"&gt;-h&lt;/span&gt; POSTGRES_IP &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; hindsight-verify hindsight-bank.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the pgvector extension and a sample recall exist in the restored copy before you delete the original dump target. Keep the dump on a different host than the Hindsight VM, and run this as a PostgreSQL admin (a role that can create databases) rather than the application role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — all-in-one container.&lt;/strong&gt; The embedded database is a named volume, so stop the container and tar the volume through a throwaway image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop hindsight
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; hindsight-pg0:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/backup alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/hindsight-bank.tgz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
docker start hindsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mount point inside the tar container (&lt;code&gt;/data&lt;/code&gt;) is arbitrary; it simply aliases the named volume so &lt;code&gt;tar&lt;/code&gt; can read it. To restore, stop the container, clear the volume, and untar the archive back into it, or point the container at the external PostgreSQL restore from Option B.&lt;/p&gt;

&lt;p&gt;Take a snapshot at three moments rather than on a calendar schedule: before changing the embedding model, before a major Hindsight or PostgreSQL upgrade, and before a &lt;code&gt;reflect&lt;/code&gt; or consolidation pass that rewrites the bank. A backup you have restored into a scratch bank is worth more than one you have only written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export, import, and migrating the memory bank
&lt;/h2&gt;

&lt;p&gt;Backup and restore protect the bank in place. Export/import moves it between Hindsight instances, and that is what a model change actually forces you to do.&lt;/p&gt;

&lt;p&gt;Changing the embedding model is not a configuration edit. The vectors in the bank were computed with the old model, and a different model — for example the bundled 384-dimensional BGE embedder versus 1024-dimensional Qwen3-Embedding-0.6B — writes into a different space and usually a different width. You cannot re-point the existing instance at the new embedder and keep the old vectors; the dimensions must be recalculated by re-embedding every memory with the new embedding model.&lt;/p&gt;

&lt;p&gt;Because the embedder and its configuration are set per Hindsight instance, the clean path is to stand up a fresh instance with the new embedding configuration and migrate the bank into it, rather than rebuilding vectors inside the one that is already serving recall. The Hindsight API can move the bank too, but for a one-off change export/import is the easier and more inspectable route:&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;# export the bank from the old instance&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;hindsight &lt;span class="se"&gt;\&lt;/span&gt;
  hindsight-admin export-bank &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bank&lt;/span&gt; hermes2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; /tmp/hermes2.zip

docker &lt;span class="nb"&gt;cp &lt;/span&gt;hindsight:/tmp/hermes2.zip ./hermes2.zip

&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; hermes2.zip
unzip &lt;span class="nt"&gt;-t&lt;/span&gt; hermes2.zip

&lt;span class="c"&gt;# import the bank into the new instance&lt;/span&gt;
docker &lt;span class="nb"&gt;cp&lt;/span&gt; ./hermes2.zip hindsight-q06:/tmp/hermes2.zip

docker &lt;span class="nb"&gt;exec &lt;/span&gt;hindsight-q06 &lt;span class="se"&gt;\&lt;/span&gt;
  hindsight-admin import-bank &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--archive&lt;/span&gt; /tmp/hermes2.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unzip -t&lt;/code&gt; verifies the archive before you ship it to the new instance. After the import, re-point Hermes at the new instance's &lt;code&gt;api_url&lt;/code&gt; in &lt;code&gt;~/.hermes/hindsight/config.json&lt;/code&gt;, restart the gateway, and run the memory validation from earlier: a retained fact should recall through the new embedder. Leave the old bank and its vectors in the old instance until the new one behaves, so you still have a working fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitor, prune, and keep the bank stable
&lt;/h2&gt;

&lt;p&gt;With the bank running, the day-to-day job is watching what the extraction model has decided to store and keeping derived state in check. Use the control plane UI (&lt;code&gt;hindsight-embed -p hermes ui start&lt;/code&gt;) to inspect observations, relevance scores, and consolidation output rather than assuming &lt;code&gt;memory status&lt;/code&gt; proves ingestion and recall are correct. Duplicated or stale memories are usually a weak extraction or an under-run consolidation, so fix the extraction tier or consolidation cadence before widening retrieval.&lt;/p&gt;

&lt;p&gt;Two maintenance traps are worth testing before you rely on the bank. First, deleting an observation does not guarantee every derived representation disappears with it — a &lt;code&gt;reflect&lt;/code&gt; fact or graph edge built from it can survive the delete, so verify deletion end to end, the same provenance concern covered in &lt;a href="https://www.glukhov.org/ai-systems/memory/self-reinforcing-memory-loops/" rel="noopener noreferrer"&gt;self-reinforcing memory loops&lt;/a&gt;. Second, keep &lt;code&gt;HINDSIGHT_API_WORKER_ID&lt;/code&gt; stable across recreations and pin a specific image tag: a task that was mid-flight when the container died stays stuck under the old worker id, and a moving tag is a poor match for a database you intend to keep.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight source and documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vectorize-io/hindsight/blob/main/skills/hindsight-docs/references/developer/installation.md" rel="noopener noreferrer"&gt;Official Hindsight installation guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vectorize-io/hindsight/blob/main/skills/hindsight-docs/references/developer/configuration.md" rel="noopener noreferrer"&gt;Hindsight configuration reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vectorize-io/hindsight/blob/main/hindsight-docs/docs-integrations/hermes.md" rel="noopener noreferrer"&gt;Hindsight Hermes integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hindsight.vectorize.io/developer/models" rel="noopener noreferrer"&gt;Hindsight inference models and reranking providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md" rel="noopener noreferrer"&gt;llama.cpp server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hermes</category>
      <category>selfhosting</category>
      <category>llamacpp</category>
      <category>ollama</category>
    </item>
    <item>
      <title>Upgrading to Go 1.27: What Breaks, What to Adopt First</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 16 Oct 2026 09:33:44 +0000</pubDate>
      <link>https://dev.to/rosgluk/upgrading-to-go-127-what-breaks-what-to-adopt-first-3mpc</link>
      <guid>https://dev.to/rosgluk/upgrading-to-go-127-what-breaks-what-to-adopt-first-3mpc</guid>
      <description>&lt;p&gt;Go 1.27 shipped in August 2026, and for most modules the upgrade is a single &lt;code&gt;go.mod&lt;/code&gt; line followed by a &lt;code&gt;go fix&lt;/code&gt; pass — but a handful of changes bite silently. Here is what actually breaks and what to adopt first.&lt;/p&gt;

&lt;p&gt;The headline changes are structural: &lt;code&gt;encoding/json&lt;/code&gt; is now backed by the new json/v2 implementation under the hood, the &lt;code&gt;uuid&lt;/code&gt; package joined the standard library, and methods can finally declare their own type parameters. None of these force a rewrite, and the Go team kept the usual compatibility promise — almost everything compiles and runs unchanged.&lt;/p&gt;

&lt;p&gt;Not everything is quiet, though. Several &lt;code&gt;GODEBUG&lt;/code&gt; settings now fail the build at their old values, &lt;code&gt;compress/flate&lt;/code&gt; produces different bytes, &lt;code&gt;go test&lt;/code&gt; enforces a new &lt;code&gt;stdversion&lt;/code&gt; check, and compiler-generated symbol names changed. If you need a syntax refresher on the parts that did not move, the &lt;a href="https://www.glukhov.org/developer-tools/cheatsheets/golang-cheatsheet/" rel="noopener noreferrer"&gt;Go cheatsheet&lt;/a&gt; stays valid; this guide covers the rest, with commands and fixes for each item.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upgrade path in one diagram
&lt;/h2&gt;

&lt;p&gt;The Go team recommends a build-first, fix-second order. &lt;code&gt;go fix&lt;/code&gt; rewrites code to new APIs and idioms, but only a clean build tells you whether any of the runtime-level changes hit you.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Install Go 1.27 toolchain] --&amp;gt; B["go build ./..."]
    B --&amp;gt; C{Build passes?}
    C -- "No: removed GODEBUG value" --&amp;gt; D[Set final default or delete the line]
    D --&amp;gt; B
    C -- Yes --&amp;gt; E["go test ./..."]
    E --&amp;gt; F{"stdversion + vet clean?"}
    F -- "No: symbol too new" --&amp;gt; G[Raise go line or swap the symbol]
    G --&amp;gt; E
    F -- Yes --&amp;gt; H["go mod tidy"]
    H --&amp;gt; I["go fix ./..."]
    I --&amp;gt; J[Review JSON golden tests and diffs]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Start with the toolchain itself. If you manage toolchains through the &lt;code&gt;go&lt;/code&gt; command, either pin the version in &lt;code&gt;go.mod&lt;/code&gt; or install explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;golang.org/dl/go1.27@latest
go1.27 download
go1.27 version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, set &lt;code&gt;GOTOOLCHAIN=go1.27.0&lt;/code&gt; in the environment, or add &lt;code&gt;toolchain go1.27.0&lt;/code&gt; to &lt;code&gt;go.mod&lt;/code&gt; and let the &lt;code&gt;go&lt;/code&gt; command fetch it. The module-focused companion on &lt;a href="https://www.glukhov.org/developer-tools/local-dev-platforms/go-workspace-structure/" rel="noopener noreferrer"&gt;Go workspace structure&lt;/a&gt; explains how the &lt;code&gt;go&lt;/code&gt; and &lt;code&gt;toolchain&lt;/code&gt; lines interact across multiple modules; the short version for a single module is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: bump the &lt;code&gt;go&lt;/code&gt; line and clear removed GODEBUG settings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go mod edit &lt;span class="nt"&gt;-go&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.27
go mod tidy
go build ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raising the &lt;code&gt;go&lt;/code&gt; directive turns on the new language features (generic methods, promoted-field keys in struct literals, wider function type inference) and makes the &lt;code&gt;go&lt;/code&gt; command strict about &lt;code&gt;GODEBUG&lt;/code&gt; settings whose support has been removed. Go 1.27 removed these permanently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;asynctimerchan&lt;/code&gt; — timer and ticker channels are now always unbuffered&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gotypesalias&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tlsunsafeekm&lt;/code&gt;, &lt;code&gt;tlsrsakex&lt;/code&gt;, &lt;code&gt;tls3des&lt;/code&gt;, &lt;code&gt;tls10server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x509keypairleaf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new rule: the &lt;code&gt;go&lt;/code&gt; command still accepts these settings in &lt;code&gt;go.mod&lt;/code&gt; (&lt;code&gt;godebug&lt;/code&gt; entries) or in source (&lt;code&gt;//go:debug&lt;/code&gt; comments) if they are set to the final default value that existed before removal. An old value — the one that disabled a behaviour change — fails the build:&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="c"&gt;//go:debug asynctimerchan=0  // build error: old value of a removed setting&lt;/span&gt;
&lt;span class="c"&gt;//go:debug asynctimerchan=1  // accepted, but delete the line instead&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your build fails here, the code depended on pre-Go-1.23 buffered timer channels. The fix is in the code, not the config: check &lt;code&gt;Timer.Stop&lt;/code&gt; and &lt;code&gt;Timer.Reset&lt;/code&gt; usage against the &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-context-cancellation-timeouts/" rel="noopener noreferrer"&gt;context, cancellation, and timeout patterns&lt;/a&gt; that rely on the synchronous channels. The same search applies to the TLS-related settings — anything pinning &lt;code&gt;tls3des=1&lt;/code&gt; or &lt;code&gt;tls10server=1&lt;/code&gt; was keeping old cipher suites alive, and Go 1.27 refuses to compile it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: run &lt;code&gt;go test&lt;/code&gt; — the stdversion trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;go build&lt;/code&gt; and &lt;code&gt;go test&lt;/code&gt; no longer agree. Go 1.27's &lt;code&gt;go test&lt;/code&gt; runs the &lt;code&gt;stdversion&lt;/code&gt; vet check by default, which reports standard-library symbols that are too new for the &lt;code&gt;go&lt;/code&gt; version in force in the referring file.&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="c"&gt;// go.mod still says "go 1.26"&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CutLast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// added in Go 1.27&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&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;p&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;file&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a 1.27 toolchain, &lt;code&gt;go build&lt;/code&gt; succeeds — the compiler has &lt;code&gt;strings.CutLast&lt;/code&gt; — while &lt;code&gt;go test&lt;/code&gt; fails the vet step because &lt;code&gt;go.mod&lt;/code&gt; still declares 1.26. With a real 1.26 toolchain the file would not build at all. The lesson is procedural: never validate an upgrade with &lt;code&gt;go build&lt;/code&gt; alone; run the full test suite, then bump the &lt;code&gt;go&lt;/code&gt; line. The vet suite in &lt;code&gt;go test&lt;/code&gt; also picked up a new &lt;code&gt;printf&lt;/code&gt; finding: &lt;code&gt;fmt.Errorf("...: %w", p)&lt;/code&gt; where &lt;code&gt;p&lt;/code&gt; is a &lt;code&gt;*E&lt;/code&gt; and &lt;code&gt;E&lt;/code&gt; implements &lt;code&gt;error&lt;/code&gt; now warns, because wrapping a pointer produces type assertions and &lt;code&gt;errors.Is&lt;/code&gt; checks that fail. That failure mode shows up in error-handling code exactly as described in the &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; patterns — check the operand type, not just the verb.&lt;/p&gt;

&lt;h2&gt;
  
  
  encoding/json: the v2 engine underneath
&lt;/h2&gt;

&lt;p&gt;The biggest under-the-hood change: &lt;code&gt;encoding/json/v2&lt;/code&gt; (experimental since Go 1.25 as &lt;code&gt;GOEXPERIMENT=jsonv2&lt;/code&gt;) graduated, and the classic &lt;code&gt;encoding/json&lt;/code&gt; package is now backed by the v2 implementation. For v1 callers, behaviour is preserved — nil slices still encode as &lt;code&gt;null&lt;/code&gt;, map keys still sort — with one visible difference: the exact text of some error messages changed. If tests or monitoring match on error strings, expect diffs.&lt;/p&gt;

&lt;p&gt;Three things can still go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Error text.&lt;/strong&gt; Any &lt;code&gt;strings.Contains(err.Error(), ...)&lt;/code&gt; assertions keyed to the old v1 wording need updating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance-sensitive paths may shift.&lt;/strong&gt; Unmarshalling is faster, but the rewrite changes which operations allocate; benchmarks flagged in CI will show it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility escape hatch.&lt;/strong&gt; &lt;code&gt;GOEXPERIMENT=nojsonv2&lt;/code&gt; restores the original v1 implementation at build time. It is temporary — plan to remove it, not to ship with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A second-order effect for golden tests: &lt;code&gt;compress/flate&lt;/code&gt;'s encoder changed in the same release, so &lt;code&gt;archive/zip&lt;/code&gt;, &lt;code&gt;compress/gzip&lt;/code&gt;, &lt;code&gt;compress/zlib&lt;/code&gt;, and &lt;code&gt;image/png&lt;/code&gt; outputs may differ byte-for-byte from Go 1.26 even though they decode identically. Regenerate compressed fixtures rather than pinning old bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating to the v2 API deliberately
&lt;/h3&gt;

&lt;p&gt;The v2 API is a separate import (&lt;code&gt;encoding/json/v2&lt;/code&gt;) with &lt;code&gt;Marshal&lt;/code&gt;, &lt;code&gt;Unmarshal&lt;/code&gt;, and an options system. The supported migration path is to make v2 calls behave like v1 first, then tighten selectively:&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="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json/v2"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json/jsontext"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Step 1: identical behaviour to encoding/json — a safe, mechanical change&lt;/span&gt;
&lt;span class="n"&gt;out&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;jsonv2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonv2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptionsV1&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c"&gt;// Step 2: opt in to one v2 default at a time&lt;/span&gt;
&lt;span class="n"&gt;out&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;jsonv2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jsonv2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptionsV1&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;jsonv2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FormatNilSliceAsNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// nil slices now encode as []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you drop &lt;code&gt;DefaultOptionsV1&lt;/code&gt;, v2 defaults apply, and they are stricter and more interoperable than v1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;v1 (&lt;code&gt;encoding/json&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;v2 default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invalid UTF-8 in strings&lt;/td&gt;
&lt;td&gt;Replaced with U+FFFD&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate names in an object&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nil slice / nil map&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[]&lt;/code&gt; / &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Struct field matching&lt;/td&gt;
&lt;td&gt;Case-insensitive fallback&lt;/td&gt;
&lt;td&gt;Case-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown object members&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;td&gt;Ignored (&lt;code&gt;RejectUnknownMembers&lt;/code&gt; to error)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map key ordering&lt;/td&gt;
&lt;td&gt;Always sorted&lt;/td&gt;
&lt;td&gt;Unsorted by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time.Duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Built-in string encoding&lt;/td&gt;
&lt;td&gt;No default encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The unsorted map keys are the one that surprises golden-file tests: v2 skips the sort because sorting costs time. When you need stable output — fixtures, snapshot tests, hashed representations — pass &lt;code&gt;json.Deterministic(true)&lt;/code&gt;, which restores sorted output for maps and structs. The &lt;a href="https://www.glukhov.org/app-architecture/data-access/comparing-go-orms-gorm-ent-bun-sqlc/" rel="noopener noreferrer"&gt;ORM comparison&lt;/a&gt; article's models all carry &lt;code&gt;json&lt;/code&gt; tags; those tags keep working in v2, but the &lt;code&gt;format&lt;/code&gt; tag option was removed during the experiment and &lt;code&gt;inline&lt;/code&gt; was renamed to &lt;code&gt;embed&lt;/code&gt;, so grep your model definitions for both before migrating. For low-level needs — streaming, token-by-token processing, custom syntax handling — the companion &lt;code&gt;encoding/json/jsontext&lt;/code&gt; package exposes an &lt;code&gt;Encoder&lt;/code&gt;/&lt;code&gt;Decoder&lt;/code&gt; state machine over JSON text.&lt;/p&gt;

&lt;h2&gt;
  
  
  stdlib &lt;code&gt;uuid&lt;/code&gt;: dropping &lt;code&gt;github.com/google/uuid&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Go 1.27 adds &lt;code&gt;uuid&lt;/code&gt; to the standard library, implementing RFC 9562. The API surface covers the common cases:&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;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;uuid&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="c"&gt;// currently equivalent to NewV4&lt;/span&gt;
&lt;span class="n"&gt;v7&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewV7&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c"&gt;// time-sortable, 48-bit timestamp + random&lt;/span&gt;
&lt;span class="n"&gt;u&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;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"550e8400-e29b-41d4-a716-446655440000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;github.com/google/uuid&lt;/code&gt; is the second most-depended-on Go module, so this migration matters at scale. The two APIs are close — types are &lt;code&gt;[16]byte&lt;/code&gt; values comparable with &lt;code&gt;==&lt;/code&gt; and usable as map keys, &lt;code&gt;String()&lt;/code&gt;, &lt;code&gt;MarshalText&lt;/code&gt;/&lt;code&gt;UnmarshalText&lt;/code&gt; round-trip — and the upstream project is preparing &lt;code&gt;go:fix inline&lt;/code&gt; shims so &lt;code&gt;go fix&lt;/code&gt; can rewrite &lt;code&gt;uuid.NewV4()&lt;/code&gt; calls to the standard package and delete the dependency.&lt;/p&gt;

&lt;p&gt;The interesting adoption decision is &lt;code&gt;NewV7&lt;/code&gt;. Version-7 UUIDs embed a millisecond timestamp in the most significant bits, so values sort in creation order (except when the system clock moves backwards). For database primary keys that means better B-tree locality than random UUIDv4, and &lt;code&gt;created_at&lt;/code&gt;-like ordering for free in the identifier itself — the same property that drives time-sortable key discussion in multi-tenant data models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language changes: generic methods and friends
&lt;/h2&gt;

&lt;p&gt;Generic methods are the release's most requested language feature: a method declaration may now declare its own type parameters.&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="c"&gt;// Go 1.27: math/rand/v2 gained a generic method alongside the function&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Rand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt; &lt;span class="n"&gt;intType&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two restrictions keep the type system decidable: interface methods may not declare type parameters, and interface methods cannot be implemented by generic methods. Alongside them, struct literal keys may now be any valid field selector (not just top-level field names), and function type inference generalizes to all contexts where a generic function is assigned to a matching function-type variable. None of these break existing code; they remove workarounds — package-level generic helper functions and explicit instantiations.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go fix&lt;/code&gt; grew modernizers for adjacent cleanups: &lt;code&gt;atomictypes&lt;/code&gt;, &lt;code&gt;embedlit&lt;/code&gt;, &lt;code&gt;slicesbackward&lt;/code&gt;, and &lt;code&gt;unsafefuncs&lt;/code&gt;. One caution from the hands-on upgrade labs: run &lt;code&gt;go fix -fix=atomictypes&lt;/code&gt; deliberately rather than in bulk on libraries, since it can change the types of package-level variables that other packages reference. The &lt;code&gt;waitgroup&lt;/code&gt; modernizer was renamed to &lt;code&gt;waitgroupgo&lt;/code&gt;; note that &lt;code&gt;sync.WaitGroup.Go&lt;/code&gt; (from 1.25) already covers most of what it detects — see the &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/bounding-concurrency-in-go/" rel="noopener noreferrer"&gt;bounded concurrency patterns&lt;/a&gt; for how it slots into worker-pool shutdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that quietly break
&lt;/h2&gt;

&lt;p&gt;These are the changes that compile fine and still cost you a release day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Function-literal symbol names.&lt;/strong&gt; The compiler now generates simpler, inlining-independent names for closures, and may merge identical function literals. Tests that assert on symbol names in tracebacks or profiles will fail; code that compares function code pointers for equality is exposed in more cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gofmt&lt;/code&gt; alignment.&lt;/strong&gt; A fix to column-alignment logic (it previously varied across CPU architectures) reflows a small number of already-formatted files. Expect whitespace-only diffs in a &lt;code&gt;gofmt ./...&lt;/code&gt; commit after the upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;go mod tidy&lt;/code&gt; rewrites &lt;code&gt;go.mod&lt;/code&gt;.&lt;/strong&gt; For modules on &lt;code&gt;go 1.27&lt;/code&gt;, &lt;code&gt;tidy&lt;/code&gt; now merges duplicate &lt;code&gt;require&lt;/code&gt; blocks into at most two blocks (direct, indirect), preserving comments. Diff your &lt;code&gt;go.mod&lt;/code&gt; and resolve any merge-conflict residue before the release commit, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceback labels.&lt;/strong&gt; Tracebacks for modules on &lt;code&gt;go 1.27&lt;/code&gt; include &lt;code&gt;runtime/pprof&lt;/code&gt; goroutine labels in the header line; disable with &lt;code&gt;GODEBUG=tracebacklabels=0&lt;/code&gt; if labels carry sensitive data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;go tool trace&lt;/code&gt; binds to localhost.&lt;/strong&gt; &lt;code&gt;-http=:6060&lt;/code&gt; now listens on localhost only; pass &lt;code&gt;-http=0.0.0.0:6060&lt;/code&gt; explicitly when you need remote access (and mind that a profiling port on a shared host is a risk — see the production profiling workflow for the hardened setup).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Experiments worth watching
&lt;/h2&gt;

&lt;p&gt;Two experimental packages shipped in 1.27, both enabled with &lt;code&gt;GOEXPERIMENT=simd&lt;/code&gt; at build time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;simd&lt;/code&gt;&lt;/strong&gt; — a portable, vector-size-agnostic API (&lt;code&gt;Int8s&lt;/code&gt;, &lt;code&gt;Float32s&lt;/code&gt;, ...) that maps to hardware instructions where available. It supports AVX/AVX2/AVX-512 on amd64, NEON on arm64, and WebAssembly SIMD. The standard library already uses this machinery internally: Go's Swiss Table map implementation benefits from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;simd/archsimd&lt;/code&gt;&lt;/strong&gt; — the architecture-specific layer for code that needs to know the vector width.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the crypto side, &lt;code&gt;crypto/mldsa&lt;/code&gt; implements the post-quantum ML-DSA signature scheme from FIPS 204 in three parameter sets (&lt;code&gt;MLDSA44&lt;/code&gt;, &lt;code&gt;MLDSA65&lt;/code&gt;, &lt;code&gt;MLDSA87&lt;/code&gt;), integrated into &lt;code&gt;crypto/x509&lt;/code&gt; and offered as TLS 1.3 signature schemes. Hybrid post-quantum key exchange can now be forced via &lt;code&gt;Config.CurvePreferences&lt;/code&gt; even when the &lt;code&gt;tlsmlkem&lt;/code&gt;/&lt;code&gt;tlssecpmlkem&lt;/code&gt; &lt;code&gt;GODEBUG&lt;/code&gt; fallbacks are set. Neither is a migration item today, but both shape the TLS configuration surface your services will grow into.&lt;/p&gt;

&lt;h2&gt;
  
  
  New APIs worth adopting immediately
&lt;/h2&gt;

&lt;p&gt;A few additions solve everyday friction and cost nothing to adopt:&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="c"&gt;// strings.CutLast / bytes.CutLast — the LastIndex dance, replaced&lt;/span&gt;
&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CutLast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a/b/c.tar.gz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// httptest.NewTestServer — in-memory fake network, auto-cleanup&lt;/span&gt;
&lt;span class="n"&gt;srv&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;NewTestServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// no real port, no defer srv.Close()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;httptest.NewTestServer&lt;/code&gt; creates an &lt;code&gt;httptest.Server&lt;/code&gt; backed by an in-memory network instead of a TCP listener and registers its own &lt;code&gt;t.Cleanup&lt;/code&gt;. Its real payoff is pairing with &lt;code&gt;testing/synctest&lt;/code&gt;: HTTP round-trips run in synthetic time, making timeout and retry tests fast and deterministic — the combination is demonstrated in the &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/testing-concurrent-go-code-synctest/" rel="noopener noreferrer"&gt;synctest concurrency testing&lt;/a&gt; article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade checklist
&lt;/h2&gt;

&lt;p&gt;The order matters more than the items; each step assumes the previous one is green.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Small module&lt;/th&gt;
&lt;th&gt;Service / monorepo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Toolchain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go install golang.org/dl/go1.27@latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pin &lt;code&gt;toolchain go1.27.0&lt;/code&gt;; align CI images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. &lt;code&gt;go.mod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go mod edit -go=1.27 &amp;amp;&amp;amp; go mod tidy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same, plus resolve &lt;code&gt;require&lt;/code&gt;-block merge diffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Removed GODEBUG&lt;/td&gt;
&lt;td&gt;Grep &lt;code&gt;//go:debug&lt;/code&gt; and &lt;code&gt;godebug:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Also audit deployed configs and flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. &lt;code&gt;go build ./...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fix compile errors&lt;/td&gt;
&lt;td&gt;Fix per-module; bisect failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. &lt;code&gt;go test ./...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fix &lt;code&gt;stdversion&lt;/code&gt;/&lt;code&gt;printf&lt;/code&gt; findings&lt;/td&gt;
&lt;td&gt;Run before the &lt;code&gt;go&lt;/code&gt; bump to catch symbol drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. &lt;code&gt;go fix ./...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Review the diff&lt;/td&gt;
&lt;td&gt;One modernizer at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. JSON/golden tests&lt;/td&gt;
&lt;td&gt;Regenerate fixtures&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;flate&lt;/code&gt;-based fixtures (zip/gzip/png) too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. &lt;code&gt;GOEXPERIMENT=nojsonv2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Skip&lt;/td&gt;
&lt;td&gt;Temporary only; file issues for what forced it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Closing notes
&lt;/h2&gt;

&lt;p&gt;Go 1.27's breaks cluster around edges most programs never touch — removed &lt;code&gt;GODEBUG&lt;/code&gt; opt-outs, symbol-name assertions, compressed-byte fixtures — while its wins land where every program lives: faster JSON, standard UUIDs, generic methods. A clean upgrade is mostly discipline: &lt;code&gt;go build&lt;/code&gt;, then &lt;code&gt;go test&lt;/code&gt;, then &lt;code&gt;go fix&lt;/code&gt;, in that order, with the &lt;code&gt;go&lt;/code&gt; line raised before the test run rather than after it. The &lt;code&gt;nojsonv2&lt;/code&gt; escape hatch and the &lt;code&gt;asynctimerchan&lt;/code&gt; acceptance-at-final-default rule buy time, but both are scheduled to disappear, so treat this upgrade as the deadline for the deferred fixes it inherits from 1.23–1.26.&lt;/p&gt;

</description>
      <category>go</category>
      <category>dev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Bounding Concurrency in Go: errgroup, Worker Pools, and Backpressure</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:53:44 +0000</pubDate>
      <link>https://dev.to/rosgluk/bounding-concurrency-in-go-errgroup-worker-pools-and-backpressure-1eh4</link>
      <guid>https://dev.to/rosgluk/bounding-concurrency-in-go-errgroup-worker-pools-and-backpressure-1eh4</guid>
      <description>&lt;p&gt;Unbounded fan-out is the default failure mode of Go concurrency: every &lt;code&gt;go&lt;/code&gt; statement is one more task in flight, with no ceiling and no owner. This article is about giving both.&lt;/p&gt;

&lt;p&gt;Cancellation and bounding are separate controls. Cancellation decides when work stops; bounding decides how much of it exists at once. A service can cancel perfectly and still fall over because ten thousand goroutines each decided, independently, that now was a good time to call the database.&lt;/p&gt;

&lt;p&gt;The pages around this one cover the other half of the problem. &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-context-cancellation-timeouts/" rel="noopener noreferrer"&gt;Go context.Context Done Right&lt;/a&gt; treats context as the control plane for stopping work; here the subject is the capacity plane — &lt;code&gt;errgroup.SetLimit&lt;/code&gt;, semaphore channels, worker pools, and bounded queues — plus the one leak shape that survives &lt;code&gt;errgroup.Wait&lt;/code&gt; even when every cancel path looks wired up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five primitives, one decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Partial results&lt;/th&gt;
&lt;th&gt;Fail-fast&lt;/th&gt;
&lt;th&gt;Backpressure&lt;/th&gt;
&lt;th&gt;Dynamic work&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sync.WaitGroup&lt;/code&gt; + &lt;code&gt;errors.Join&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;wait-all batches where every result counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errgroup.WithContext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fan-out where the first error ends the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errgroup.SetLimit(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (blocks &lt;code&gt;Go&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;bounded fail-fast fan-out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semaphore channel&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;yes (blocks acquire)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;custom admission control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker pool&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;yes (queue fills)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;steady streams, long-lived workers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two questions pick the row. First: when one task fails, do you still want the others' results? If yes, fail-fast group semantics discard work you needed. Second: does work arrive continuously, or is it a fixed batch you can count before launching? Groups handle batches; pools and queues handle streams.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;errors.Join&lt;/code&gt; is what makes the plain &lt;code&gt;WaitGroup&lt;/code&gt; row viable — it collects every worker's error instead of keeping only the first, which pairs with the boundary-translation rules in &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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leak that survives errgroup.Wait
&lt;/h2&gt;

&lt;p&gt;The best-known errgroup failure mode has nothing to do with bounding. It is a cancellation gap, and it hides behind code that looks correct:&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&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;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// unbuffered&lt;/span&gt;

&lt;span class="k"&gt;go&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="c"&gt;// producer — nobody owns this goroutine&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&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="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// consumer — group member&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;results&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;r&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;5&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;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;"save failed"&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="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="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// returns at the consumer's error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the consumer returns, &lt;code&gt;gctx&lt;/code&gt; cancels and &lt;code&gt;Wait&lt;/code&gt; returns. The producer is parked on &lt;code&gt;results &amp;lt;- i&lt;/code&gt; — and context cancellation does not unblock a channel send. Nobody owns that goroutine, so it stays parked for the life of the process. Run this shape in a loop and the leak is exactly linear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Live goroutines after 500 iterations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Send without a &lt;code&gt;ctx.Done&lt;/code&gt; arm&lt;/td&gt;
&lt;td&gt;501 (+500 leaked, one per iteration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send wrapped in &lt;code&gt;select&lt;/code&gt; with &lt;code&gt;&amp;lt;-gctx.Done()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;502 (+1 baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// the fix: every channel operation inside a cancellable task&lt;/span&gt;
&lt;span class="c"&gt;// gets a Done arm&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;gctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same gap exists on the receive side. The rule is mechanical: every channel operation inside code that can be cancelled gets a &lt;code&gt;&amp;lt;-ctx.Done()&lt;/code&gt; arm, and every goroutine has an owner that waits on it or cancels it. &lt;code&gt;goleak&lt;/code&gt; in CI catches the cases review misses — it slots into the same automated quality gate as the tools in &lt;a href="https://www.glukhov.org/developer-tools/code-quality/linters-for-go/" rel="noopener noreferrer"&gt;Go Linters: Essential Tools for Code Quality&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two related shapes are worth naming. If the producer &lt;em&gt;is&lt;/em&gt; a group member, the same code produces not a leak but a deadlock — &lt;code&gt;Wait&lt;/code&gt; blocks forever waiting for the parked send, which at least fails loudly. And a producer that sends with &lt;code&gt;select&lt;/code&gt; but receives on a channel whose consumer has quit leaks the same way; &lt;code&gt;Done&lt;/code&gt; arms belong on both sides.&lt;/p&gt;

&lt;h2&gt;
  
  
  errgroup.SetLimit: bounded fail-fast fan-out
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SetLimit&lt;/code&gt; turns an errgroup into an admission-controlled group. Each &lt;code&gt;Go&lt;/code&gt; call beyond the limit blocks until a slot frees:&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&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="kt"&gt;error&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;process&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;item&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three semantics matter in production. &lt;code&gt;Go&lt;/code&gt; blocks, so the loop above applies backpressure to the &lt;em&gt;producer&lt;/em&gt; — that is usually what you want, but it means the caller's goroutine can stall, so a request handler feeding a large batch needs its own timeout budget around the whole loop. &lt;code&gt;SetLimit(0)&lt;/code&gt; blocks every &lt;code&gt;Go&lt;/code&gt; call forever. And changing the limit while goroutines are active panics — the limit is fixed for the life of the group.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SetLimit&lt;/code&gt; inherits group semantics: the first error cancels the context and &lt;code&gt;Wait&lt;/code&gt; returns the first error, discarding results of work still in flight. It is the right primitive when failing the whole operation on first failure is correct — fetching one page of data, calling a set of independent health checks, fanning a request out to shards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semaphore channels: backpressure as a feature
&lt;/h2&gt;

&lt;p&gt;A buffered channel of capacity &lt;em&gt;n&lt;/em&gt; is a semaphore with no dependencies:&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;sem&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="m"&gt;8&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;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt; &lt;span class="c"&gt;// blocks when 8 tasks are in flight&lt;/span&gt;
    &lt;span class="k"&gt;go&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="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;defer&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="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&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;item&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;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The acquire happens before the &lt;code&gt;go&lt;/code&gt; statement, so goroutines are only ever created when a slot exists. The buffer size is the contract: it is simultaneously the concurrency ceiling and the queue of pending admissions.&lt;/p&gt;

&lt;p&gt;The primitive earns its place when you need admission control the groups do not express — context-aware acquisition, per-downstream pools, or partial results on failure:&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;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;// slot acquired&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="c"&gt;// caller gave up before getting in&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;Measured on the same workload, the semaphore channel and &lt;code&gt;SetLimit(8)&lt;/code&gt; finish within 1.5% of each other — the choice between them is semantics, not performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker pools: for streams, not batches
&lt;/h2&gt;

&lt;p&gt;A fixed pool of long-lived workers separates admission (the queue) from execution (the workers):&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;func&lt;/span&gt; &lt;span class="n"&gt;pool&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;workers&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&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="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;func&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;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&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="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pools fit work that arrives continuously — queue consumers, polling loops, request workers — where a per-batch group would be created and torn down endlessly. Since Go 1.25, &lt;code&gt;sync.WaitGroup.Go&lt;/code&gt; removes the &lt;code&gt;Add&lt;/code&gt;/&lt;code&gt;Done&lt;/code&gt; pairing boilerplate for the common shape. The shutdown contract is the part to get right: closing &lt;code&gt;jobs&lt;/code&gt; terminates workers after the queue drains; cancelling &lt;code&gt;ctx&lt;/code&gt; abandons queued work. Both are legitimate, but only one can be the documented behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounded queues: block or drop
&lt;/h2&gt;

&lt;p&gt;A buffered channel is also a queue with a ceiling. Once the buffer fills, the producer blocks — and that moment is the design decision. Blocking propagates pressure upstream to whoever feeds the queue; dropping sheds load at the cost of lost work. A monitoring pipeline can drop; an order pipeline must block or spill to durable storage.&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;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;// admitted&lt;/span&gt;
&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="c"&gt;// queue full: drop policy lives here&lt;/span&gt;
    &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;Whatever the policy, make it explicit and measured. A silent &lt;code&gt;default&lt;/code&gt; branch is where events go to disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What bounding costs — measured
&lt;/h2&gt;

&lt;p&gt;The same workload, 10,000 tasks of 5 ms simulated I/O each, Go 1.27.1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Peak goroutines&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unbounded &lt;code&gt;go&lt;/code&gt; per task&lt;/td&gt;
&lt;td&gt;~10,000 launched&lt;/td&gt;
&lt;td&gt;18 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errgroup.SetLimit(8)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semaphore channel (cap 8)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The unbounded run wins on wall time because nothing in the experiment pushes back — ten thousand timer goroutines simply sleep in parallel. That is exactly the trap. The cost of unbounded fan-out is never CPU in your own process; it is ten thousand simultaneous connections, a downstream that starts timing out under the burst, and a memory graph that follows the queue depth of whoever you are calling. On a real dependency, the unbounded run does not finish in 18 ms — it times out. The bounded runs pay 6.6 seconds because 10,000 tasks ÷ 8 slots × 5 ms is 6.25 s of unavoidable serialization, and the cap is the point: it turns an uncontrolled burst into a predictable 6.25-second drain.&lt;/p&gt;

&lt;p&gt;One refinement matters when the cap is higher than a handful of slots: keep the cap at or below what the downstream can sustain concurrently, and derive it from that limit (connection pool size, rate limit, worker capacity) rather than from a feeling about "reasonable" parallelism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring and testing bounded code
&lt;/h2&gt;

&lt;p&gt;Three signals cover most of it. Goroutine count trends (&lt;code&gt;/sched/goroutines:goroutines&lt;/code&gt; exported via the Prometheus Go collector) catch leaks as a slope. Scheduler latency (&lt;code&gt;/sched/latencies:seconds&lt;/code&gt;) catches saturation — runnable goroutines waiting for CPU time is real pressure regardless of the count. A pprof delta of two goroutine dumps pinpoints where the parked goroutines live.&lt;/p&gt;

&lt;p&gt;For the tests, bounded workers are the case &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; was built for: fake time makes a full 10,000-task drain run in milliseconds, &lt;code&gt;synctest.Wait&lt;/code&gt; replaces sleep-and-hope for quiescence, and a bubble fails fast when a worker stays durably blocked — the leak repro above dies in a test bubble instead of in production.&lt;/p&gt;

&lt;p&gt;At service scale, the same bounding question reappears between services, where the semaphore becomes a rate limit and the circuit breaker guards the downstream — &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/circuit-breaker-pattern-in-go/" rel="noopener noreferrer"&gt;Circuit Breaker Pattern in Go&lt;/a&gt; covers that layer, and &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; shows the queue-backed forms the orchestration tier takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Work to run concurrently] --&amp;gt; B{Fixed batch or continuous stream?}
    B -- continuous stream --&amp;gt; C[Worker pool or bounded queue]
    B -- fixed batch --&amp;gt; D{Partial results needed&amp;lt;br/&amp;gt;on first failure?}
    D -- yes --&amp;gt; E[WaitGroup + errors.Join&amp;lt;br/&amp;gt;or semaphore channel]
    D -- no --&amp;gt; F[errgroup.WithContext]
    F --&amp;gt; G{Need a concurrency ceiling?}
    G -- yes --&amp;gt; H[errgroup.SetLimit]
    G -- no --&amp;gt; I[group without limit&amp;lt;br/&amp;gt;only when the batch is provably small]
    C --&amp;gt; J{Full queue: block or drop?}
    J -- block --&amp;gt; K[Backpressure upstream]
    J -- drop --&amp;gt; L[Shed load, count the drops]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The table and the flowchart compress to one habit: before writing &lt;code&gt;go&lt;/code&gt;, name the ceiling and the owner. The ceiling is &lt;code&gt;SetLimit&lt;/code&gt;, a semaphore, or a bounded queue; the owner is a &lt;code&gt;Wait&lt;/code&gt;, a &lt;code&gt;ctx.Done&lt;/code&gt; arm on every channel operation, or both. Every goroutine-creation site that can answer those two names in one glance is one that will not page anyone at 3 a.m.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture hub — API design, code structure, and integration patterns&lt;/a&gt;&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.Context Done Right: Cancellation, Timeouts, and Values&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>dev</category>
      <category>go</category>
    </item>
    <item>
      <title>Clone a VM in Virt-Manager with virt-clone and virt-sysprep</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 09 Oct 2026 11:36:19 +0000</pubDate>
      <link>https://dev.to/rosgluk/clone-a-vm-in-virt-manager-with-virt-clone-and-virt-sysprep-5g07</link>
      <guid>https://dev.to/rosgluk/clone-a-vm-in-virt-manager-with-virt-clone-and-virt-sysprep-5g07</guid>
      <description>&lt;p&gt;Virt-Manager (the libvirt GUI) copies a KVM disk and assigns the clone a UUID and MAC. The guest keeps its machine-id and SSH host keys until virt-sysprep clears them.&lt;/p&gt;

&lt;p&gt;This walkthrough uses an Ubuntu 26.04 guest and a 14 GB qcow2 image stored at &lt;code&gt;/mnt/extra2/vms&lt;/code&gt;. The disk commands are the same for other Linux guests. virt-sysprep does not operate on Windows guests.&lt;/p&gt;

&lt;p&gt;The host already needs KVM and libvirt. &lt;a href="https://www.glukhov.org/developer-tools/virtualization/install-kvm-ubuntu-24-04/" rel="noopener noreferrer"&gt;Install KVM on Ubuntu 24.04&lt;/a&gt; is that setup, and it pulls in &lt;code&gt;virtinst&lt;/code&gt;, which provides &lt;code&gt;virt-clone&lt;/code&gt;. &lt;code&gt;virt-sysprep&lt;/code&gt;, &lt;code&gt;virt-customize&lt;/code&gt;, &lt;code&gt;virt-cat&lt;/code&gt;, and &lt;code&gt;guestfish&lt;/code&gt; come from a separate package:&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;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; libguestfs-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What actually clones a VM
&lt;/h2&gt;

&lt;p&gt;libvirt splits the job across three tools. &lt;code&gt;virt-clone&lt;/code&gt; copies the disk images and defines a new domain with the same virtual hardware. It changes the domain name, the UUID, and the host-side MAC. Passwords, the hostname, static addresses, and files inside the guest are unchanged, which is the behaviour &lt;a href="https://manpages.ubuntu.com/manpages/resolute/man1/virt-clone.1.html" rel="noopener noreferrer"&gt;virt-clone(1)&lt;/a&gt; documents.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;virt-sysprep&lt;/code&gt; opens the disk with libguestfs and edits the guest filesystem. The &lt;a href="https://libguestfs.org/virt-sysprep.1.html" rel="noopener noreferrer"&gt;virt-sysprep(1)&lt;/a&gt; manual is explicit that the edit is in place: there is no output-file flag, so the image you pass to &lt;code&gt;-a&lt;/code&gt; is the image that changes. Copy or clone first if you still need the source.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;qemu-img info&lt;/code&gt; reports whether that file is standalone or the top of a backing chain. &lt;code&gt;qemu-img convert&lt;/code&gt; writes a new file with the chain resolved.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[guest shut down] --&amp;gt;|virt-clone| B[new domain and disk]
    B --&amp;gt;|qemu-img info| C{backing file?}
    C --&amp;gt;|yes| D[qemu-img convert]
    C --&amp;gt;|no| E[standalone qcow2]
    D --&amp;gt; E
    E --&amp;gt;|copy, then virt-sysprep| F[template]
    F --&amp;gt;|per-instance customize| G[imported guest]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Flatten before sysprep when a backing line is present. Sysprep the copy, then derive each running guest from that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloning the disk in Virt-Manager
&lt;/h2&gt;

&lt;p&gt;Shut the guest down. &lt;code&gt;virt-clone&lt;/code&gt; rejects a running domain, and a copy of a live qcow2 is not a consistent filesystem.&lt;/p&gt;

&lt;p&gt;In Virt-Manager, right-click the guest and choose &lt;strong&gt;Clone&lt;/strong&gt;. Set the new name. On the storage step, give each disk a new path. Leaving the original path in place defines a second domain that opens the same file, so both guests write the same blocks.&lt;/p&gt;

&lt;p&gt;The new file lands beside the source. A default pool uses &lt;code&gt;/var/lib/libvirt/images/&lt;/code&gt;. A disk that already lives on &lt;code&gt;/mnt/extra2/vms&lt;/code&gt; is cloned into that directory. Note the path &lt;code&gt;virt-clone&lt;/code&gt; prints, or read it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virsh domblklist ubuntu26.04-base14gb-clone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command-line equivalent, with the name and the disk path chosen explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virt-clone &lt;span class="nt"&gt;--original&lt;/span&gt; ubuntu26.04-base14gb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; ubuntu26.04-base14gb-clone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clone.qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--auto-clone&lt;/code&gt; invents the name and the disk path. That is convenient for a throwaway copy. A template path is easier to keep when you pass &lt;code&gt;--name&lt;/code&gt; and &lt;code&gt;--file&lt;/code&gt; yourself. Omit &lt;code&gt;--mac&lt;/code&gt;, or pass &lt;code&gt;--mac RANDOM&lt;/code&gt;, and virt-clone generates an address in the QEMU range &lt;code&gt;52:54:00:xx:xx:xx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--reflink&lt;/code&gt; asks for a lightweight copy. The Ubuntu 26.04 virt-clone manual limits that flag to raw images on the same btrfs filesystem, so it skips these qcow2 files. A qcow2 overlay is a separate setup, covered with &lt;code&gt;qemu-img create -b&lt;/code&gt; below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does your clone have a backing file?
&lt;/h2&gt;

&lt;p&gt;Run this on the new disk before you treat it as independent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img info &lt;span class="nt"&gt;--backing-chain&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clone.qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A standalone image lists &lt;code&gt;file format&lt;/code&gt;, &lt;code&gt;virtual size&lt;/code&gt;, and &lt;code&gt;disk size&lt;/code&gt;. An overlay also lists a &lt;code&gt;backing file:&lt;/code&gt; line pointing at the image it reads through. &lt;code&gt;virtual size&lt;/code&gt; is the guest's disk size, here about 14 GB. &lt;code&gt;disk size&lt;/code&gt; is how much space the file currently occupies, which is small for a fresh overlay and grows as the guest writes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;virt-clone&lt;/code&gt; is not the usual source of that line. An overlay comes from &lt;code&gt;qemu-img create -b&lt;/code&gt;, from a disk-only snapshot, or from a tool that created a qcow2 delta on purpose. The info check is what tells those cases apart from a full copy.&lt;/p&gt;

&lt;p&gt;Keep the overlay only when the base will stay where it is and will not be written again. Every write from the clone lands in the small file; reads that miss in the overlay come from the base. Moving or editing the base breaks every guest that names it.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    subgraph linked
        O[base.qcow2 read-only]
        C1[vm1.qcow2 deltas only]
        C2[vm2.qcow2 deltas only]
        C1 --&amp;gt;|backing file| O
        C2 --&amp;gt;|backing file| O
    end
    subgraph flat
        F1[vm1-flat.qcow2 standalone]
        F2[vm2-flat.qcow2 standalone]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;To make a standalone file, convert the overlay. &lt;code&gt;-p&lt;/code&gt; prints progress. The destination is a new file; the overlay and the base are left as they are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img convert &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-base14gb-clone.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-base14gb-flat.qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;qemu-img info&lt;/code&gt; on the flat file and confirm there is no &lt;code&gt;backing file:&lt;/code&gt; line. A template is that standalone file. Overlays are how you stamp guests from it later, not the template itself.&lt;/p&gt;

&lt;p&gt;A btrfs reflink is a third case. &lt;code&gt;cp --reflink=auto&lt;/code&gt; can share physical extents between two files, and &lt;code&gt;qemu-img info&lt;/code&gt; still shows no backing file, because each path is a complete image. That copy is independent at the qcow2 layer even though the filesystem has not duplicated every block yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  virt-sysprep: resetting the identity
&lt;/h2&gt;

&lt;p&gt;Copy the standalone image, then sysprep the copy. &lt;code&gt;--reflink=auto&lt;/code&gt; shares extents when the filesystem supports it and otherwise makes a normal copy, including on ext4. &lt;code&gt;-n&lt;/code&gt; is a dry run that discards its writes:&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;cp&lt;/span&gt; &lt;span class="nt"&gt;--reflink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-base14gb-flat.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2

virt-sysprep &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2
virt-sysprep &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt; ubuntu-template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a default Ubuntu pool the image is often mode &lt;code&gt;0600&lt;/code&gt; and owned by &lt;code&gt;libvirt-qemu&lt;/code&gt;. The manual recommends giving your user write access rather than running as root. When the qemu user owns the file, &lt;code&gt;sudo virt-sysprep -a ...&lt;/code&gt; is the invocation that can open it. The guest must be shut down, and nothing else should have the image open.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--hostname&lt;/code&gt; is a customize option, and customize is part of the default operation set, so it runs in the same invocation. There is no separate default operation that clears &lt;code&gt;/etc/hostname&lt;/code&gt; on its own. &lt;code&gt;--operations&lt;/code&gt; replaces that default set when you pass it; &lt;code&gt;--operations defaults,-ssh-userdir&lt;/code&gt; keeps the defaults and turns one operation off.&lt;/p&gt;

&lt;p&gt;These are the operations that decide whether the clone collides with the original. The list is the one in the current &lt;a href="https://libguestfs.org/virt-sysprep.1.html" rel="noopener noreferrer"&gt;virt-sysprep manual&lt;/a&gt;, which is worth reading because a short &lt;code&gt;--operations&lt;/code&gt; list silently drops the rest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Effect on the guest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;machine-id&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Clears the machine ID. systemd generates a new one on the next boot when &lt;code&gt;/etc/machine-id&lt;/code&gt; is empty.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ssh-hostkeys&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Removes &lt;code&gt;/etc/ssh/ssh_host_*&lt;/code&gt;. The next boot creates new host keys.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ssh-userdir&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Removes &lt;code&gt;.ssh&lt;/code&gt; under &lt;code&gt;/root&lt;/code&gt; and &lt;code&gt;/home/*&lt;/code&gt;, including &lt;code&gt;authorized_keys&lt;/code&gt; and private keys stored in the guest.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dhcp-client-state&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Removes DHCP client leases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;udev-persistent-net&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Removes udev rules that map an old MAC to a fixed name such as &lt;code&gt;eth0&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;net-hwaddr&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Removes &lt;code&gt;HWADDR&lt;/code&gt; from Fedora and RHEL &lt;code&gt;ifcfg-*&lt;/code&gt; files. It does not edit Ubuntu netplan.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lvm-uuids, lvm-system-devices&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Assigns new LVM UUIDs and removes &lt;code&gt;/etc/lvm/devices/system.devices&lt;/code&gt;, which otherwise pins the guest to the source disk's WWIDs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logfiles, bash-history, tmp-files&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Removes logs, shell history, and temp files.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ssh-userdir&lt;/code&gt; is the one that surprises people cloning a machine they still log into. The template should not carry the source's private keys, so leave the operation on and inject a key per instance later. For a one-off copy that must keep the existing &lt;code&gt;authorized_keys&lt;/code&gt;, pass &lt;code&gt;--operations defaults,-ssh-userdir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user-account&lt;/code&gt; is not in the default set, and passwords stay unless you pass &lt;code&gt;--password&lt;/code&gt; or &lt;code&gt;--root-password&lt;/code&gt;. &lt;code&gt;fs-uuids&lt;/code&gt; is also off by default. The manual says it does not update every reference, including &lt;code&gt;/etc/fstab&lt;/code&gt;, and that enabling it is likely to make the guest unbootable.&lt;/p&gt;

&lt;p&gt;Ubuntu Server images usually have cloud-init, and virt-sysprep has no cloud-init operation. An old instance id under &lt;code&gt;/var/lib/cloud&lt;/code&gt; makes the clone skip first-boot setup. Clean it on the template, and tell cloud-init to leave the hostname that &lt;code&gt;--hostname&lt;/code&gt; wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virt-customize &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-command&lt;/span&gt; &lt;span class="s1"&gt;'if command -v cloud-init &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then cloud-init clean --logs --seed; fi'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="s1"&gt;'/etc/cloud/cloud.cfg.d/99-preserve-hostname.cfg:preserve_hostname: true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same manual warns against cloning a guest that uses internal full-disk encryption for distribution: every copy shares the volume key. It also notes that deleted files are unlinked, not scrubbed. &lt;code&gt;--scrub&lt;/code&gt; overwrites a named file, and &lt;code&gt;virt-sparsify&lt;/code&gt; can drop freed space from the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the clone gets its IP address
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;virt-sysprep&lt;/code&gt; has no operation that rewrites an address. A guest that used DHCP asks again. The clone has a new MAC, an empty machine-id, and no old lease file, so the DHCP server on libvirt's &lt;code&gt;default&lt;/code&gt; network treats it as a new client.&lt;/p&gt;

&lt;p&gt;A static &lt;code&gt;addresses:&lt;/code&gt; block in netplan is copied with the disk, and both guests then claim it. Read the files before the first boot. &lt;code&gt;virt-ls&lt;/code&gt; and &lt;code&gt;virt-cat&lt;/code&gt; are the one-shot tools; &lt;code&gt;guestfish --ro -a &amp;lt;disk&amp;gt; -i&lt;/code&gt; is the same inspection as a shell, after which &lt;code&gt;ls /etc/netplan&lt;/code&gt; and &lt;code&gt;cat&lt;/code&gt; run against the guest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virt-ls &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 /etc/netplan
virt-cat &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/netplan/00-installer-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installer images often use &lt;code&gt;00-installer-config.yaml&lt;/code&gt; or &lt;code&gt;01-netcfg.yaml&lt;/code&gt;. Cloud images use &lt;code&gt;50-cloud-init.yaml&lt;/code&gt;, which cloud-init will recreate unless network configuration is disabled. Netplan merges every YAML file in the directory, so a leftover static file still applies after you add a DHCP file.&lt;/p&gt;

&lt;p&gt;To make the template a DHCP client, remove those files and upload one replacement. Use the interface name &lt;code&gt;virt-cat&lt;/code&gt; showed. On a guest installed under KVM that name is often &lt;code&gt;ens3&lt;/code&gt; or &lt;code&gt;enp1s0&lt;/code&gt;, and &lt;code&gt;virt-clone&lt;/code&gt; keeps the same virtual PCI slot, so the name stays valid:&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; /tmp/99-dhcp.yaml &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;'
network:
  version: 2
  ethernets:
    ens3:
      dhcp4: true
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;virt-customize &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="s1"&gt;'/etc/netplan/*.yaml'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upload&lt;/span&gt; /tmp/99-dhcp.yaml:/etc/netplan/99-dhcp.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="s1"&gt;'/etc/cloud/cloud.cfg.d/99-disable-network.cfg:network: {config: disabled}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a static address, gateway, and DNS, use the same upload with the YAML from &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/how-to-change-static-ip-address-in-ubuntu/" rel="noopener noreferrer"&gt;How to Change a Static IP Address in Ubuntu Server&lt;/a&gt;. Each instance then needs its own file, applied to the instance disk rather than the template.&lt;/p&gt;

&lt;p&gt;The name in the YAML has to exist inside the guest. A disk moved from physical hardware often names the NIC &lt;code&gt;enp2s0&lt;/code&gt;, while the same system under virtio presents a different name, and netplan then configures an interface that is not there. For a template that should boot on more than one machine type, pin the kernel to &lt;code&gt;eth0&lt;/code&gt; and use that key in netplan. Ubuntu sources &lt;code&gt;/etc/default/grub.d/*.cfg&lt;/code&gt; while building &lt;code&gt;grub.cfg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virt-customize &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="s1"&gt;'/etc/default/grub.d/99-eth0.cfg:GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX net.ifnames=0 biosdevname=0"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-command&lt;/span&gt; &lt;span class="s1"&gt;'update-grub'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;update-grub&lt;/code&gt; runs inside the libguestfs appliance. If &lt;code&gt;grub-probe&lt;/code&gt; cannot see the boot disk there, the command fails and the cmdline is unchanged. In that case, set the interface name in netplan to the name the guest already uses, and skip the &lt;code&gt;eth0&lt;/code&gt; switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-instance customization with virt-customize
&lt;/h2&gt;

&lt;p&gt;Boot instances from copies of the clean file. Do not point a domain at the template itself.&lt;/p&gt;

&lt;p&gt;A full copy has no runtime dependency on the template:&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;cp&lt;/span&gt; &lt;span class="nt"&gt;--reflink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-vm1.qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An overlay stores only the blocks vm1 writes. &lt;code&gt;-F qcow2&lt;/code&gt; records the base format so QEMU does not have to probe it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img create &lt;span class="nt"&gt;-f&lt;/span&gt; qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/extra2/vms/ubuntu26.04-vm1.qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base path is stored inside the overlay. It has to keep that path, and it has to stay read-only, for every guest built this way.&lt;/p&gt;

&lt;p&gt;The virt-sysprep security notes describe a second pass on each new disk. Sysprep writes a random seed into the template; guests cloned from that file would otherwise start with the same seed, which makes first-boot SSH host keys and TCP sequence numbers predictable. &lt;code&gt;--enable customize&lt;/code&gt; rewrites the seed. &lt;code&gt;--hostname&lt;/code&gt; and &lt;code&gt;--ssh-inject&lt;/code&gt; are customize options, so they belong on the same command. Substitute the account that actually exists in the image for &lt;code&gt;ubuntu&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virt-sysprep &lt;span class="nt"&gt;--enable&lt;/span&gt; customize &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/extra2/vms/ubuntu26.04-vm1.qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt; vm1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssh-inject&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu:file:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.ssh/id_ed25519.pub"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hand the instance file to libvirt, not the template. If you created it as your user outside a storage pool, &lt;code&gt;libvirt-qemu&lt;/code&gt; needs read access or the domain fails at startup with a permission error on the qcow2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing into Virt-Manager
&lt;/h2&gt;

&lt;p&gt;For each instance disk, open Virt-Manager, choose &lt;strong&gt;New Virtual Machine&lt;/strong&gt;, and select &lt;strong&gt;Import existing disk image&lt;/strong&gt;. Point it at &lt;code&gt;ubuntu26.04-vm1.qcow2&lt;/code&gt;. The OS entry should be Ubuntu 26.04 when the host's osinfo database has it.&lt;/p&gt;

&lt;p&gt;The matching short id is &lt;code&gt;ubuntu26.04&lt;/code&gt;. Check the host database before relying on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osinfo-query os | &lt;span class="nb"&gt;grep &lt;/span&gt;ubuntu26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that prints nothing, the installed database is older than the 26.04 entry. &lt;code&gt;--os-variant ubuntu24.04&lt;/code&gt; still selects a virtio-friendly machine type for a guest that is already installed. &lt;code&gt;--import&lt;/code&gt; tells virt-install the disk already contains an OS.&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;virt-install &lt;span class="nt"&gt;--name&lt;/span&gt; ubuntu26.04-vm1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 4096 &lt;span class="nt"&gt;--vcpus&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disk&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/extra2/vms/ubuntu26.04-vm1.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--import&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--os-variant&lt;/span&gt; ubuntu26.04 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; &lt;span class="nv"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--noautoconsole&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--network network=default&lt;/code&gt; attaches the NAT network whose dnsmasq instance hands out the DHCP lease. After boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virsh domifaddr ubuntu26.04-vm1 &lt;span class="nt"&gt;--source&lt;/span&gt; lease
virsh dumpxml ubuntu26.04-vm1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;mac"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--source lease&lt;/code&gt; reads that dnsmasq table. It stays empty for a static address, for a bridge, or for a NIC that is not on a libvirt virtual network. Those guests need &lt;code&gt;qemu-guest-agent&lt;/code&gt; in the image and &lt;code&gt;virsh domifaddr ubuntu26.04-vm1 --source agent&lt;/code&gt;, or an ARP lookup with &lt;code&gt;--source arp&lt;/code&gt;. &lt;code&gt;virsh net-dhcp-leases default&lt;/code&gt; lists every lease on the NAT network when you want the pool view rather than one domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a clone strategy: full copy, overlay, or reflink
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Space per VM&lt;/th&gt;
&lt;th&gt;Time to create&lt;/th&gt;
&lt;th&gt;Constraint&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;Full copy, or &lt;code&gt;cp --reflink=auto&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;about 14 GB logical; reflink shares blocks until they change&lt;/td&gt;
&lt;td&gt;minutes, or near-instant with reflink&lt;/td&gt;
&lt;td&gt;reflink needs a filesystem that supports it, such as btrfs&lt;/td&gt;
&lt;td&gt;a guest that should outlive the template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qcow2 overlay&lt;/td&gt;
&lt;td&gt;megabytes at first&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;base path must remain, and the base must stay read-only&lt;/td&gt;
&lt;td&gt;many similar guests from one sealed image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;virt-clone --reflink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;raw image, shared extents&lt;/td&gt;
&lt;td&gt;near-instant&lt;/td&gt;
&lt;td&gt;raw disks on the same btrfs filesystem; qcow2 is skipped&lt;/td&gt;
&lt;td&gt;raw images already stored on btrfs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A guest you expect to keep for months is a full copy or a reflink. A short-lived guest can be an overlay, as long as the base file is still there when it runs. &lt;a href="https://www.glukhov.org/developer-tools/virtualization/proxmox-virtualization/" rel="noopener noreferrer"&gt;Proxmox&lt;/a&gt; is the other self-hosted path when you want a datacenter-style template and clone model on its own web UI. &lt;a href="https://www.glukhov.org/developer-tools/virtualization/vm-manager-multipass-cheatsheet/" rel="noopener noreferrer"&gt;Multipass&lt;/a&gt; launches Ubuntu cloud images for a single user and applies cloud-init itself, which is a different workflow from sealing a disk you already installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting: when the clone will not boot
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No address.&lt;/strong&gt; &lt;code&gt;virt-cat&lt;/code&gt; the netplan file and compare the interface key with &lt;code&gt;ip link&lt;/code&gt; on the console. A &lt;code&gt;match: macaddress&lt;/code&gt; entry still set to the source MAC drops the new NIC. A key that names an interface the guest does not have does the same. Fix the file on the disk, or apply the &lt;code&gt;eth0&lt;/code&gt; cmdline above and rebuild the instance from the template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Address up, same as the original.&lt;/strong&gt; The static &lt;code&gt;addresses:&lt;/code&gt; block is still in one of the netplan files. &lt;code&gt;virt-sysprep&lt;/code&gt; did not remove it. Upload a replacement and delete the other YAML files in &lt;code&gt;/etc/netplan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH says the host key changed, or accepts the clone as the original.&lt;/strong&gt; Host keys are shared when &lt;code&gt;ssh-hostkeys&lt;/code&gt; did not run on the file the domain opens. Confirm with &lt;code&gt;virsh domblklist&lt;/code&gt; that the domain uses the sysprepped disk, then check &lt;code&gt;qemu-img info&lt;/code&gt; so you did not sysprep an overlay while the guest boots from a different path. If the clone kept the original IP on purpose, clients that stored the old key in &lt;code&gt;known_hosts&lt;/code&gt; will warn until that line is removed. That warning is the regeneration the manual describes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key login fails on a clone of a machine you could reach before.&lt;/strong&gt; Default sysprep removed &lt;code&gt;.ssh&lt;/code&gt; from the home directories. Inject a key with &lt;code&gt;--ssh-inject&lt;/code&gt;, or re-run with &lt;code&gt;--operations defaults,-ssh-userdir&lt;/code&gt; if this copy should have kept them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LVM volumes missing after boot.&lt;/strong&gt; The guest has &lt;code&gt;/etc/lvm/devices/system.devices&lt;/code&gt; from the source, and the virtual disk's WWID changed. The default &lt;code&gt;lvm-system-devices&lt;/code&gt; operation removes that file. A hand-picked &lt;code&gt;--operations&lt;/code&gt; list has to include it, and &lt;code&gt;lvm-uuids&lt;/code&gt;, when the guest uses LVM.&lt;/p&gt;

&lt;h2&gt;
  
  
  One clone or a template
&lt;/h2&gt;

&lt;p&gt;A single extra machine is &lt;code&gt;virt-clone&lt;/code&gt;, a backing-file check, and &lt;code&gt;virt-sysprep&lt;/code&gt; on the new disk. A fleet is one sealed standalone file, then a copy or an overlay per guest, with &lt;code&gt;--enable customize&lt;/code&gt; on that guest's own disk before it boots.&lt;/p&gt;

&lt;p&gt;Overlays make the sealed file part of the runtime path of every guest that names it. Full copies and reflinks do not. Pick that dependency with the lifetime of the guest, and leave &lt;code&gt;fs-uuids&lt;/code&gt; off so the bootloader and &lt;code&gt;/etc/fstab&lt;/code&gt; still match the filesystem.&lt;/p&gt;

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

&lt;ul&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;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/local-dev-platforms/install-linux-ubuntu-24-04/" rel="noopener noreferrer"&gt;How to Install Ubuntu 24.04 &amp;amp; useful tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/virtualization/gnome-boxes-linux-virtual-machines-manager/" rel="noopener noreferrer"&gt;GNOME Boxes: Features, Advantages, Challenges, and Alternatives&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://manpages.ubuntu.com/manpages/resolute/man1/virt-clone.1.html" rel="noopener noreferrer"&gt;virt-clone(1)&lt;/a&gt; on Ubuntu 26.04, including &lt;code&gt;--auto-clone&lt;/code&gt;, &lt;code&gt;--file&lt;/code&gt;, and &lt;code&gt;--reflink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://libguestfs.org/virt-sysprep.1.html" rel="noopener noreferrer"&gt;virt-sysprep(1)&lt;/a&gt; operations, in-place edits, and the random-seed note&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://libguestfs.org/virt-customize.1.html" rel="noopener noreferrer"&gt;virt-customize(1)&lt;/a&gt; for &lt;code&gt;--hostname&lt;/code&gt;, &lt;code&gt;--ssh-inject&lt;/code&gt;, &lt;code&gt;--upload&lt;/code&gt;, and &lt;code&gt;--run-command&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://qemu.readthedocs.io/en/master/tools/qemu-img.html" rel="noopener noreferrer"&gt;qemu-img&lt;/a&gt; &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;convert&lt;/code&gt;, and &lt;code&gt;create -b&lt;/code&gt; / &lt;code&gt;-F&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://manpages.ubuntu.com/manpages/resolute/man1/virt-install.1.html" rel="noopener noreferrer"&gt;virt-install(1)&lt;/a&gt; &lt;code&gt;--import&lt;/code&gt; and &lt;code&gt;--os-variant&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gitlab.com/libosinfo/osinfo-db/-/commit/6f01a968803a30c7e5da631b0205c5982b20b842" rel="noopener noreferrer"&gt;osinfo-db Ubuntu 26.04 entry&lt;/a&gt;, short id &lt;code&gt;ubuntu26.04&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/machine-id.html" rel="noopener noreferrer"&gt;machine-id(5)&lt;/a&gt; on how an empty &lt;code&gt;/etc/machine-id&lt;/code&gt; is replaced at boot&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>virtualization</category>
      <category>kvm</category>
    </item>
    <item>
      <title>Self-Hosted S3 Alternatives in 2026 (RustFS, Garage, SeaweedFS, Ceph)</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Thu, 08 Oct 2026 08:35:20 +0000</pubDate>
      <link>https://dev.to/rosgluk/self-hosted-s3-alternatives-in-2026-rustfs-garage-seaweedfs-ceph-4klp</link>
      <guid>https://dev.to/rosgluk/self-hosted-s3-alternatives-in-2026-rustfs-garage-seaweedfs-ceph-4klp</guid>
      <description>&lt;p&gt;Amazon S3 has become the de facto API for object storage. After the open source MinIO repository was archived in April 2026, seven credible self-hosted alternatives remain.&lt;/p&gt;

&lt;p&gt;These options are not interchangeable. Some are full object-storage engines that own their data layout; others are thin gateways that expose files you already have. That split — native store versus filesystem-backed gateway — decides more about backups, recovery, and migrations than any feature list does.&lt;/p&gt;

&lt;p&gt;This article compares RustFS, Garage, SeaweedFS, Ceph RGW, VersityGW, S3Proxy, and rclone. Each section covers the storage model, the S3 feature coverage, where the project fits, and the limitation that usually decides the choice. A decision guide for single-server and multi-server deployments closes the article.&lt;/p&gt;

&lt;p&gt;For the broader picture — object storage, databases, search, and AI-native data layers — see the &lt;a href="https://www.glukhov.org/data-infrastructure/" rel="noopener noreferrer"&gt;Data Infrastructure for AI Systems&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Hosted S3 Storage Comparison
&lt;/h2&gt;

&lt;p&gt;The most important question is whether you need a distributed object store or an S3 interface in front of storage that already exists.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Software&lt;/th&gt;
&lt;th&gt;Storage model&lt;/th&gt;
&lt;th&gt;Distributed&lt;/th&gt;
&lt;th&gt;S3 compatibility&lt;/th&gt;
&lt;th&gt;Plain files on disk&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Best use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RustFS&lt;/td&gt;
&lt;td&gt;Native object store&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High, tested subset&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;General MinIO replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Garage&lt;/td&gt;
&lt;td&gt;Native object store&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Good, narrower API&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Small distributed clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SeaweedFS&lt;/td&gt;
&lt;td&gt;Distributed blob and file store&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Huge file counts and mixed storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ceph RGW&lt;/td&gt;
&lt;td&gt;Ceph object gateway&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Large storage infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VersityGW&lt;/td&gt;
&lt;td&gt;S3 gateway over POSIX&lt;/td&gt;
&lt;td&gt;Backend dependent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;S3 over normal files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3Proxy&lt;/td&gt;
&lt;td&gt;S3 translation layer&lt;/td&gt;
&lt;td&gt;Backend dependent&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Yes with filesystem-nio2&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Gateway and protocol translation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rclone serve s3&lt;/td&gt;
&lt;td&gt;S3 gateway&lt;/td&gt;
&lt;td&gt;Backend dependent&lt;/td&gt;
&lt;td&gt;Basic, experimental&lt;/td&gt;
&lt;td&gt;Yes with local backend&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Simple and lightweight deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RustFS is the closest direct replacement for a conventional MinIO deployment. Garage fits small distributed clusters. SeaweedFS and Ceph solve broader storage problems. VersityGW can expose an existing POSIX filesystem through S3 while leaving the object payloads as ordinary files. For a feature-by-feature matrix of Garage, MinIO, and AWS S3, see &lt;a href="https://www.glukhov.org/data-infrastructure/object-storage/garage-vs-minio-vs-s3/" rel="noopener noreferrer"&gt;Garage vs MinIO vs AWS S3&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Different Types of S3 Storage
&lt;/h2&gt;

&lt;p&gt;A native object store owns its storage layout:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Application] --&amp;gt;|S3 API| B[Object Store]
    B --&amp;gt; C[Internal Metadata]
    B --&amp;gt; D[Chunks or Object Layout]
    C --&amp;gt; E[Disks]
    D --&amp;gt; E&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;RustFS, Garage, Ceph RGW, and SeaweedFS belong to this family. The files visible on the underlying disks are implementation details, and applications should not manipulate them directly.&lt;/p&gt;

&lt;p&gt;A filesystem-backed S3 gateway has a different relationship with storage:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Application] --&amp;gt;|S3 API| B[S3 Gateway]
    B --&amp;gt; C[POSIX Filesystem]
    C --&amp;gt; D[bucket]
    D --&amp;gt; E[path]
    E --&amp;gt; F[ordinary-file.dat]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;With software such as VersityGW, the filesystem can remain the authoritative representation. An object such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://archive/documents/2026/report.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can correspond directly to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/storage/archive/documents/2026/report.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  RustFS: The Closest MinIO Replacement
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt; is a distributed object storage system written in Rust and released under the Apache-2.0 license. It provides an S3-oriented operating model familiar to MinIO users, which makes it the first project to evaluate when replacing MinIO.&lt;/p&gt;

&lt;p&gt;Its current S3 implementation covers the operations most applications expect: buckets, objects, multipart uploads, conditional requests, object metadata, tags, versioning, lifecycle management, object lock, presigned URLs, bucket policies, CORS, notifications, replication configuration, and server-side encryption.&lt;/p&gt;

&lt;p&gt;RustFS publishes an &lt;a href="https://docs.rustfs.com/en/reference/s3-compatibility" rel="noopener noreferrer"&gt;S3 compatibility matrix&lt;/a&gt; that is a tested subset, not a claim of complete Amazon S3 coverage. The snapshot verified against commit &lt;code&gt;1e6f5f1e&lt;/code&gt; on 9 August 2026 lists 455 implemented standard tests, 5 lifecycle-behavior tests, 17 unimplemented standard tests, and 270 excluded cases that do not block the compatibility gate. The executable lists under &lt;code&gt;scripts/s3-tests&lt;/code&gt; are the source of truth when those counts move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where RustFS fits
&lt;/h3&gt;

&lt;p&gt;RustFS is a candidate when an application expects a proper S3 object store.&lt;/p&gt;

&lt;p&gt;Typical uses include application object storage, backup repositories, media storage, AI and data-pipeline artifacts, internal S3 infrastructure, Kubernetes workloads, and replacement of existing MinIO endpoints.&lt;/p&gt;

&lt;p&gt;It works with AWS SDKs and common S3 clients. Applications normally need a custom endpoint, credentials, region settings where required, and path-style addressing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The important limitation
&lt;/h3&gt;

&lt;p&gt;RustFS owns the storage representation. The files below its data directory are RustFS internals.&lt;/p&gt;

&lt;p&gt;That makes RustFS the wrong answer when the requirement is that every S3 object remain an ordinary file you can inspect, copy, &lt;code&gt;rsync&lt;/code&gt;, or recover without the object-storage software. That requirement selects a filesystem-backed gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Garage: Distributed S3 Without Ceph-Scale Complexity
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://garagehq.deuxfleurs.fr/" rel="noopener noreferrer"&gt;Garage&lt;/a&gt; is an S3-compatible distributed object store for small and medium self-hosted installations. Deuxfleurs develops it under the AGPL v3 licence and has run it in production since the project's initial releases in 2020.&lt;/p&gt;

&lt;p&gt;Garage is built to run across several ordinary servers, including servers in different physical locations. Replication is part of the design.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Applications] --&amp;gt; B[S3 Endpoint]
    B --&amp;gt; C[Garage Node 1]
    B --&amp;gt; D[Garage Node 2]
    B --&amp;gt; E[Garage Node 3]
    C &amp;lt;--&amp;gt; D
    D &amp;lt;--&amp;gt; E
    E &amp;lt;--&amp;gt; C&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Garage is attractive when you have three small servers rather than a rack of dedicated storage hardware. A working setup path, from Docker through cluster layout, replication, and TLS via a reverse proxy, is in the &lt;a href="https://www.glukhov.org/data-infrastructure/object-storage/garage-quickstart/" rel="noopener noreferrer"&gt;Garage quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Garage fits
&lt;/h3&gt;

&lt;p&gt;Garage makes sense for homelabs, small hosting platforms, geographically distributed servers, replicated backups, and clusters where Ceph would be excessive.&lt;/p&gt;

&lt;p&gt;Its scope is narrower than AWS S3. Garage's compatibility table leaves out bucket policies, ACLs, and bucket versioning. Choose Garage over RustFS when lightweight multi-node replication is the primary requirement. Choose RustFS when broader S3 behavior and MinIO-like application compatibility matter more.&lt;/p&gt;

&lt;h2&gt;
  
  
  SeaweedFS: More Than an Object Store
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/seaweedfs/seaweedfs" rel="noopener noreferrer"&gt;SeaweedFS&lt;/a&gt; started by storing enormous numbers of files efficiently. It is now a distributed storage platform exposing S3, a FUSE filesystem, WebDAV, SFTP, and HDFS. &lt;a href="https://github.com/seaweedfs/seaweedfs/releases" rel="noopener noreferrer"&gt;SeaweedFS 4.48&lt;/a&gt; was released on 28 September 2026.&lt;/p&gt;

&lt;p&gt;Volume servers pack blobs into append-only volume files. Masters track volumes rather than individual files. A filer provides hierarchical filesystem semantics. The design targets workloads with millions or billions of relatively small objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 support
&lt;/h3&gt;

&lt;p&gt;SeaweedFS documents versioning, object lock, lifecycle rules, tagging, CORS, checksums, presigned URLs, multipart uploads, bucket policies, IAM, STS, and several server-side encryption modes.&lt;/p&gt;

&lt;p&gt;The same cluster can serve S3 table buckets and a built-in Iceberg REST catalog, so Spark, Trino, DuckDB, and similar engines can use Iceberg tables without a separate Hive Metastore or Glue. The project's one-command S3 example listens on port 8333.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where SeaweedFS fits
&lt;/h3&gt;

&lt;p&gt;Consider SeaweedFS for web crawlers and archives, image and document repositories, billions of small objects, data lakes, mixed S3 and filesystem workloads, and storage that grows by adding volume servers.&lt;/p&gt;

&lt;p&gt;There are more components to understand than in a straightforward RustFS installation. SeaweedFS can provide filesystem semantics through its filer and FUSE mount, but objects are stored inside its volume format. It does not satisfy the requirement that every S3 object remain an independent ordinary file on the host filesystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ceph RGW: The Infrastructure-Scale Option
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ceph.io/" rel="noopener noreferrer"&gt;Ceph&lt;/a&gt; is broader than S3 storage. A cluster can provide block devices through RBD, a filesystem through CephFS, and object storage through RADOS Gateway (RGW). RGW presents an S3-compatible API backed by RADOS.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Applications] --&amp;gt;|S3| B[Ceph RGW]
    B --&amp;gt; C[RADOS]
    C --&amp;gt; D[OSD 1]
    C --&amp;gt; E[OSD 2]
    C --&amp;gt; F[OSD 3]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Ceph supports replication, erasure coding, failure domains, multiple gateways, multisite deployments, access policies, and cluster management tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Ceph fits
&lt;/h3&gt;

&lt;p&gt;Ceph makes sense when object storage is one part of a larger storage platform: Kubernetes persistent volumes, virtual-machine disks, shared filesystems, large S3 repositories, and clusters that need explicit failure domains.&lt;/p&gt;

&lt;p&gt;For a single server with several terabytes, installing Ceph only to obtain an S3 endpoint is difficult to justify. If you already operate Ceph, RGW is the obvious S3 choice. If you do not, RustFS or Garage reaches useful object storage with less machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  VersityGW: S3 Over Ordinary Files
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/versity/versitygw" rel="noopener noreferrer"&gt;VersityGW&lt;/a&gt; is the option to evaluate when the filesystem itself matters. It is an Apache-2.0 licensed S3 gateway that can expose POSIX filesystems, ScoutFS, Azure Blob Storage, other S3 services, and custom backends.&lt;/p&gt;

&lt;p&gt;Its POSIX backend maps S3 buckets and objects onto directories and files. 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;s3://website-assets/images/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;maps to a path such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/storage/website-assets/images/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;logo.png&lt;/code&gt; payload remains a normal file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why plain files are useful
&lt;/h3&gt;

&lt;p&gt;You can inspect storage with ordinary Unix tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /storage &lt;span class="nt"&gt;-type&lt;/span&gt; f
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /storage/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read an object without the S3 server:&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; /storage/archive/2026/source.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conventional filesystem backup tools apply directly: &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;restic&lt;/code&gt;, &lt;code&gt;borg&lt;/code&gt;, &lt;code&gt;tar&lt;/code&gt;, and &lt;code&gt;zfs send&lt;/code&gt;. If the gateway software disappears, the payloads are still files in a directory tree.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 metadata on a POSIX backend
&lt;/h3&gt;

&lt;p&gt;S3 metadata does not fit in a POSIX file by itself. VersityGW stores it in extended attributes by default. Since March 2026, object metadata is a single &lt;code&gt;user.metadata&lt;/code&gt; xattr holding JSON, because one xattr per key hit filesystem key-length limits. A &lt;code&gt;versitygw utils convert-xattr-metadata&lt;/code&gt; command rewrites older per-key attributes. Reads still fall back to the old layout when &lt;code&gt;user.metadata&lt;/code&gt; is absent.&lt;/p&gt;

&lt;p&gt;Filesystems without xattrs can use &lt;code&gt;--sidecar&lt;/code&gt; (or &lt;code&gt;VGW_META_SIDECAR&lt;/code&gt;) to keep metadata in a separate directory tree. The project marks sidecar and &lt;code&gt;--nometa&lt;/code&gt; as less tested than xattrs. &lt;code&gt;--nometa&lt;/code&gt; disables metadata storage and is aimed at read-only hosting of a dataset that already exists; bucket policies and ACLs are metadata, so they are unavailable in that mode.&lt;/p&gt;

&lt;p&gt;Object versions can live in a separate directory. The project's POSIX example passes &lt;code&gt;--versioning-dir&lt;/code&gt; for older versions while the current payload stays at its natural path.&lt;/p&gt;

&lt;p&gt;Changing a file behind the gateway can leave S3 metadata, including ETags, inconsistent with the new bytes. A workable operating model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application access:
    Read through S3
    Write through S3

Administrator access:
    Read the filesystem directly
    Inspect the filesystem directly
    Back up the filesystem directly
    Avoid modifying objects behind the gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VersityGW 1.8.0, already the version a September 2026 COSI driver release targeted, adds a standalone IAM service (&lt;code&gt;versitygw iam&lt;/code&gt;), STS &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;, and IAM policy conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3Proxy: A General Translation Layer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/gaul/s3proxy" rel="noopener noreferrer"&gt;S3Proxy&lt;/a&gt; implements the S3 API and translates requests onto several backends: local files, Azure Blob Storage, Google Cloud Storage, OpenStack Swift, SFTP, and other S3 services. It requires Java 17 or newer.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[S3 client] --&amp;gt; B[S3Proxy]
    B --&amp;gt; C[filesystem-nio2]
    C --&amp;gt; D[Local filesystem]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The recommended on-disk provider is &lt;code&gt;filesystem-nio2&lt;/code&gt;. The older &lt;code&gt;filesystem&lt;/code&gt; provider is deprecated. Object keys become filesystem paths, and metadata uses user extended attributes, so the payloads stay ordinary files.&lt;/p&gt;

&lt;p&gt;The filesystem backend refuses object versioning and server-side encryption. Versioning stays unsupported (&lt;code&gt;supportsVersioning()&lt;/code&gt; is false, and those requests remain HTTP 501) because a version layout written to a real disk would become a format the project would have to keep. Server-side encryption is refused for the same class of reason: reporting AES256 over a plaintext file someone can read would be a false claim. The in-memory &lt;code&gt;transient-nio2&lt;/code&gt; backend implements both, and the data dies with the process. The project README also lists bucket policies, object tagging, and ACLs other than &lt;code&gt;private&lt;/code&gt; and &lt;code&gt;public-read&lt;/code&gt; as unsupported.&lt;/p&gt;

&lt;p&gt;S3Proxy is the tool when you need that translation across backends. For a straightforward S3-to-POSIX gateway, VersityGW is the smaller surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  rclone serve s3: Lightweight but Experimental
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rclone.org/commands/rclone_serve_s3/" rel="noopener noreferrer"&gt;rclone &lt;code&gt;serve s3&lt;/code&gt;&lt;/a&gt; exposes an rclone backend through an S3-compatible API. The documentation still labels the command experimental.&lt;/p&gt;

&lt;p&gt;For a local filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone serve s3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--addr&lt;/span&gt; :9000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auth-key&lt;/span&gt; ACCESS_KEY,SECRET_KEY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;local&lt;/span&gt;:/srv/s3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--addr&lt;/code&gt;, the server binds &lt;code&gt;127.0.0.1:8080&lt;/code&gt;. &lt;code&gt;--auth-key&lt;/code&gt; enables Signature Version 4. Multipart uploads are written in part-number order to a temporary object and renamed into place on completion, so a failed upload does not replace an existing object.&lt;/p&gt;

&lt;p&gt;Objects written through a &lt;code&gt;local&lt;/code&gt; backend become files under that path. The same command can sit in front of other rclone remotes. It is a reasonable choice for internal tooling, a lab, or a temporary migration hop. It is a weak permanent S3 layer for many unrelated production applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About MinIO?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/minio/minio" rel="noopener noreferrer"&gt;MinIO GitHub repository&lt;/a&gt; was archived on 13 February 2026, briefly unarchived, and archived again on 25 April 2026. It is read-only. The repository notice points operators at AIStor, MinIO's commercial distribution, and states that the community edition is source-only.&lt;/p&gt;

&lt;p&gt;An existing MinIO deployment can keep running, particularly when it is isolated and its behavior is already understood. The full timeline, the operational risk, and a phased migration plan are in &lt;a href="https://www.glukhov.org/data-infrastructure/object-storage/minio-dead/" rel="noopener noreferrer"&gt;MinIO CE in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the S3 behavior of the system you may be leaving, &lt;a href="https://www.glukhov.org/data-infrastructure/object-storage/minio-vs-aws-s3/" rel="noopener noreferrer"&gt;MinIO as an AWS S3 alternative&lt;/a&gt; documents how MinIO maps onto S3. The &lt;a href="https://www.glukhov.org/data-infrastructure/object-storage/minio-cheatsheet/" rel="noopener noreferrer"&gt;MinIO command line cheatsheet&lt;/a&gt; is the audit companion for a cluster that is still up. For a new installation, an actively maintained alternative is the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plain Files vs Native Object Storage
&lt;/h2&gt;

&lt;p&gt;Use RustFS, Garage, SeaweedFS, or Ceph when the object store should control durability, metadata, distribution, and layout. That buys richer S3 semantics, replication or erasure coding, horizontal scaling, and versioning where the project implements it. The cost is dependence on the engine's on-disk representation.&lt;/p&gt;

&lt;p&gt;Use VersityGW, S3Proxy, or rclone when an existing filesystem should remain authoritative. That buys plain recoverable files, conventional backups, direct inspection, and compatibility with ZFS, XFS, ext4, and NFS. Distributed durability then has to come from the filesystem or the storage array, not from the S3 process. On a single server or a NAS, that trade is often the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Self-Hosted S3 Server
&lt;/h2&gt;

&lt;p&gt;For a single server that should resemble MinIO, start with RustFS.&lt;/p&gt;

&lt;p&gt;For three or more modest servers where replication is the main requirement, and the application can live without bucket policies and versioning, choose Garage.&lt;/p&gt;

&lt;p&gt;For enormous numbers of small objects, mixed filesystem access, or a data-lake workload that may grow into Iceberg tables, consider SeaweedFS.&lt;/p&gt;

&lt;p&gt;When the same cluster must also provide block devices and a distributed filesystem, choose Ceph RGW.&lt;/p&gt;

&lt;p&gt;When the requirement is specifically an S3 API over ordinary files — a NAS, an archive, a document repository, a crawler, or a backup target — start with VersityGW.&lt;/p&gt;

&lt;p&gt;When the job is translating S3 onto Azure, GCS, Swift, SFTP, or another S3 service, use S3Proxy.&lt;/p&gt;

&lt;p&gt;When the deployment is small, temporary, and can tolerate an experimental server, &lt;code&gt;rclone serve s3&lt;/code&gt; may be enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Architecture for a Single Storage Server
&lt;/h2&gt;

&lt;p&gt;If applications should reach the data only through S3, RustFS owns the object layer and the filesystem is an implementation detail:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Applications] --&amp;gt;|S3| B[RustFS]
    B --&amp;gt; C[Local Storage]
    C --&amp;gt; D[Filesystem or RAID]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If the files themselves must survive the gateway, keep the data in a POSIX tree:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Applications] --&amp;gt;|S3| B[VersityGW]
    B --&amp;gt; C[POSIX Filesystem]
    C --&amp;gt; D[ext4, XFS, ZFS, NFS]
    D --&amp;gt; E[Plain Files]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;In the second design, redundancy is a ZFS mirror, RAID, snapshots, filesystem replication, or a conventional backup tool. The S3 service can be replaced without rewriting the data format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Maintained self-hosted S3 still exists after MinIO's public repository went read-only. The choice that matters is whether S3 owns the bytes or only fronts files that already exist. Every comparison above follows from that answer.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;RustFS on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.rustfs.com/en/reference/s3-compatibility" rel="noopener noreferrer"&gt;RustFS S3 compatibility matrix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://garagehq.deuxfleurs.fr/" rel="noopener noreferrer"&gt;Garage project site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seaweedfs/seaweedfs" rel="noopener noreferrer"&gt;SeaweedFS on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seaweedfs/seaweedfs/releases" rel="noopener noreferrer"&gt;SeaweedFS releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ceph.io/" rel="noopener noreferrer"&gt;Ceph project site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/versity/versitygw" rel="noopener noreferrer"&gt;VersityGW on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/versity/versitygw/wiki/POSIX-metadata" rel="noopener noreferrer"&gt;VersityGW POSIX metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gaul/s3proxy" rel="noopener noreferrer"&gt;S3Proxy on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rclone.org/commands/rclone_serve_s3/" rel="noopener noreferrer"&gt;rclone serve s3 documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/minio/minio" rel="noopener noreferrer"&gt;MinIO on GitHub (archived)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>s3</category>
      <category>minio</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Go Web Frameworks in 2026: net/http, chi, Gin, Echo, Fiber Compared</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 05 Oct 2026 23:53:52 +0000</pubDate>
      <link>https://dev.to/rosgluk/go-web-frameworks-in-2026-nethttp-chi-gin-echo-fiber-compared-bm3</link>
      <guid>https://dev.to/rosgluk/go-web-frameworks-in-2026-nethttp-chi-gin-echo-fiber-compared-bm3</guid>
      <description>&lt;p&gt;Go ships a production-capable HTTP server in its standard library, and since Go 1.22 the built-in router handles method matching and path parameters on its own. The framework question has narrowed, not disappeared.&lt;/p&gt;

&lt;p&gt;Five stacks cover almost every Go backend decision in 2026: the standard library's &lt;code&gt;net/http&lt;/code&gt;, chi, Gin, Echo, and Fiber. Each takes a different position on the same tradeoff — what the library gives you versus what you owe it in dependencies, conventions, and ecosystem compatibility. This article compares them on routing power, handler ergonomics, middleware, and measured performance, then ends with a decision table rather than a single verdict.&lt;/p&gt;

&lt;p&gt;If you are starting from zero and want the full build process around your choice — validation, authentication, testing, deployment — the step-by-step version lives in &lt;a href="https://www.glukhov.org/app-architecture/api-architecture/implementing-api-in-go/" rel="noopener noreferrer"&gt;Building REST APIs in Go&lt;/a&gt;. Here the focus stays on the stacks themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The post-1.22 ServeMux reset
&lt;/h2&gt;

&lt;p&gt;The reason the framework debate changed shape is one addition to the standard library. Go 1.22 gave &lt;code&gt;http.ServeMux&lt;/code&gt; method-aware patterns and path parameters:&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;mux&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;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET /users/{id}"&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;id&lt;/span&gt; &lt;span class="o"&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;PathValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST /users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before this, any API needing &lt;code&gt;GET /users/{id}&lt;/code&gt; required a third-party router, which is how Gin, chi, and Echo became defaults. In 2026, the stdlib covers static routes, method matching, single-segment and wildcard parameters, and middleware via plain function wrapping. What it still does not give you: route groups, composable middleware chains, struct binding, or per-route param types.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;net/http (stdlib)&lt;/th&gt;
&lt;th&gt;chi v5.3.2&lt;/th&gt;
&lt;th&gt;Gin v1.12.0&lt;/th&gt;
&lt;th&gt;Echo v4.16.0&lt;/th&gt;
&lt;th&gt;Fiber v2.52.15&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP engine&lt;/td&gt;
&lt;td&gt;net/http&lt;/td&gt;
&lt;td&gt;net/http&lt;/td&gt;
&lt;td&gt;net/http&lt;/td&gt;
&lt;td&gt;net/http&lt;/td&gt;
&lt;td&gt;fasthttp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handler signature&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func(w, r)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func(w, r)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func(c *gin.Context)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func(c echo.Context) error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func(c *fiber.Ctx) error&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;http.Handler compatible&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;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware ecosystem&lt;/td&gt;
&lt;td&gt;growing, stdlib-shaped&lt;/td&gt;
&lt;td&gt;stdlib-shaped&lt;/td&gt;
&lt;td&gt;largest, Gin-specific&lt;/td&gt;
&lt;td&gt;large, Echo-specific&lt;/td&gt;
&lt;td&gt;growing, fasthttp-specific&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binding/validation&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;built-in tags&lt;/td&gt;
&lt;td&gt;built-in&lt;/td&gt;
&lt;td&gt;via helpers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2&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;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~6&lt;/td&gt;
&lt;td&gt;~8&lt;/td&gt;
&lt;td&gt;~4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All five use trie- or radix-style routers with path parameters and grouped routes. The structural divide is the last row of the handler-signature story: chi and stdlib speak plain &lt;code&gt;http.Handler&lt;/code&gt;; Gin, Echo, and Fiber wrap requests in their own context type. Fiber is the outlier once more because it sits on fasthttp, not &lt;code&gt;net/http&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I benchmarked
&lt;/h2&gt;

&lt;p&gt;Rather than quoting synthetic router benchmarks, I ran the same service on all five stacks. The rig:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go 1.27.1&lt;/strong&gt;, Gin v1.12.0, chi v5.3.2, Echo v4.16.0, Fiber v2.52.15&lt;/li&gt;
&lt;li&gt;Intel Core Ultra 7 265, Linux, 20 threads, localhost, no TLS&lt;/li&gt;
&lt;li&gt;Three routes per stack: &lt;code&gt;GET /health&lt;/code&gt;, &lt;code&gt;GET /users/{id}&lt;/code&gt;, &lt;code&gt;GET /api/v1/users/{uid}/orders/{oid}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Load: 8 keep-alive workers for 3 seconds against the two-parameter route, after a warmup pass&lt;/li&gt;
&lt;li&gt;Every handler marshals a small JSON document per request with &lt;code&gt;encoding/json&lt;/code&gt; — so the numbers include serialization, not just routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stdlib version of the hot route:&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;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET /api/v1/users/{uid}/orders/{oid}"&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;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderBody&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;PathValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uid"&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;PathValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"oid"&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;and the Fiber equivalent:&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;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/users/:uid/orders/:oid"&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;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ctx&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderBody&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;Params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uid"&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;Params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"oid"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two honesty notes before the numbers. First, this is a localhost micro-workload: no database, no TLS handshake, no middleware chain. Real services bottleneck on I/O — the framework's share of a request that queries PostgreSQL via &lt;a href="https://www.glukhov.org/app-architecture/data-access/comparing-go-orms-gorm-ent-bun-sqlc/" rel="noopener noreferrer"&gt;GORM, Ent, Bun, or sqlc&lt;/a&gt; is a rounding error. Second, each stack got its own server process and an identical handler shape, so the comparison is between stacks, not between hand-tuned configurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Throughput on the dynamic two-parameter route:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Requests/sec&lt;/th&gt;
&lt;th&gt;Avg latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fiber (fasthttp)&lt;/td&gt;
&lt;td&gt;205,958&lt;/td&gt;
&lt;td&gt;0.034 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Echo&lt;/td&gt;
&lt;td&gt;169,826&lt;/td&gt;
&lt;td&gt;0.042 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gin&lt;/td&gt;
&lt;td&gt;169,715&lt;/td&gt;
&lt;td&gt;0.042 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;net/http ServeMux&lt;/td&gt;
&lt;td&gt;169,708&lt;/td&gt;
&lt;td&gt;0.042 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chi&lt;/td&gt;
&lt;td&gt;163,017&lt;/td&gt;
&lt;td&gt;0.044 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The four &lt;code&gt;net/http&lt;/code&gt;-based stacks land within 4% of each other. Fiber's fasthttp engine is about 21% ahead on this localhost workload — real, but the kind of gap that a single database roundtrip (0.5–5 ms) makes irrelevant in production.&lt;/p&gt;

&lt;p&gt;To see where the per-request cost actually goes, I also benchmarked handler invocation without the network, calling each router directly through &lt;code&gt;http.Handler.ServeHTTP&lt;/code&gt; (Fiber through its fasthttp handler):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Time/op&lt;/th&gt;
&lt;th&gt;Bytes/op&lt;/th&gt;
&lt;th&gt;Allocs/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gin&lt;/td&gt;
&lt;td&gt;289 ns&lt;/td&gt;
&lt;td&gt;192 B&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Echo&lt;/td&gt;
&lt;td&gt;333 ns&lt;/td&gt;
&lt;td&gt;192 B&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;net/http ServeMux&lt;/td&gt;
&lt;td&gt;430 ns&lt;/td&gt;
&lt;td&gt;240 B&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fiber&lt;/td&gt;
&lt;td&gt;541 ns*&lt;/td&gt;
&lt;td&gt;192 B&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chi&lt;/td&gt;
&lt;td&gt;592 ns&lt;/td&gt;
&lt;td&gt;897 B&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* The Fiber figure carries harness overhead — the test has to copy the request into fasthttp's reusable context each iteration, which the real server avoids by reading from the wire. Treat it as an upper bound; the server-level throughput above is the fair comparison for Fiber.&lt;/p&gt;

&lt;p&gt;The allocation profile is the more durable story. Gin and Echo pool their request contexts, so a request through them costs 3 allocations. The stdlib's 5 allocations go to the request-scoped plumbing itself. chi stores route parameters in the request &lt;code&gt;context.Context&lt;/code&gt;, which shows up as 897 B and 7 allocations per request — the price of staying a plain &lt;code&gt;http.Handler&lt;/code&gt; with rich route info.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fiber and the fasthttp tradeoff
&lt;/h2&gt;

&lt;p&gt;Fiber is the fastest stack on the server benchmark, and the speed comes with a compatibility bill. fasthttp is a from-scratch HTTP implementation with its own request/response types, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;net/http&lt;/code&gt; middleware does not attach.&lt;/strong&gt; Any library expecting an &lt;code&gt;http.Handler&lt;/code&gt; or &lt;code&gt;*http.Request&lt;/code&gt; needs a conversion or a Fiber-native port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No HTTP/2.&lt;/strong&gt; fasthttp does not implement it; Go's &lt;code&gt;net/http&lt;/code&gt; gets h2 from the standard library for free over TLS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket, streaming, and &lt;code&gt;http.Flusher&lt;/code&gt;-style tricks&lt;/strong&gt; all take the fasthttp path, which differs from the stdlib behavior most Go documentation assumes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What Fiber buys in exchange: the fastest numbers in the table above, an Express-like API, and lower allocations than the framework benches suggest once the wire parsing advantage is included. It fits when responses are small, routing dominates, HTTP/2 is terminated upstream, and the middleware you need exists in the Fiber ecosystem. Those conditions hold for some edge services and hold for almost nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  chi: the boring middle path
&lt;/h2&gt;

&lt;p&gt;chi is a router and a middleware pipeline, nothing more. Handlers are &lt;code&gt;func(w, r)&lt;/code&gt;, middleware is &lt;code&gt;func(http.Handler) http.Handler&lt;/code&gt;, and everything written for the stdlib attaches without adapters:&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;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRouter&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;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RealIP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Recoverer&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;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1"&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;r&lt;/span&gt; &lt;span class="n"&gt;chi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;)&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;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{uid}/orders/{oid}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getOrder&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;That is the whole pitch: route groups and a middleware stack on top of the standard interface. The allocation table prices it honestly — richer route context per request than the stdlib alone, in exchange for composable middleware and nested routes. For services that want structure without a framework identity, chi plus &lt;code&gt;net/http&lt;/code&gt; is the configuration most Go teams converge on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gin vs Echo, in code
&lt;/h2&gt;

&lt;p&gt;Gin and Echo are near-twins on performance — within noise in both benchmarks — so the choice between them is API taste and error-handling philosophy.&lt;/p&gt;

&lt;p&gt;Echo centralizes errors in an &lt;code&gt;HTTPErrorHandler&lt;/code&gt;. Handlers return &lt;code&gt;error&lt;/code&gt;, and one function maps errors to responses:&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;func&lt;/span&gt; &lt;span class="n"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;echo&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="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;o&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;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&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;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uid"&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;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"oid"&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="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHTTPError&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;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order 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;return&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;JSON&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;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&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;Gin handlers return nothing; errors accumulate on the context and you render them where you choose:&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;func&lt;/span&gt; &lt;span class="n"&gt;getOrder&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;gin&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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;o&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;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&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;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uid"&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;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"oid"&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&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;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"order not found"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&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;JSON&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;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&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;Echo's model maps cleanly onto &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; with wrapped errors and centralized boundary handling — one place translates &lt;code&gt;errors.Is&lt;/code&gt; results into status codes. Gin's model matches the more common pattern of deciding per handler. Beyond errors, Gin's deciding advantages are ecosystem size and built-in struct binding with validation tags; Echo's are a consistent context API and stronger built-in middleware. Both integrate Swagger generation through their own adapters — the per-framework setup is covered in &lt;a href="https://www.glukhov.org/app-architecture/api-architecture/adding-swagger-to-api-in-go/" rel="noopener noreferrer"&gt;Adding Swagger to Your Go API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One more axis: context propagation. Gin and Echo wrap &lt;code&gt;context.Context&lt;/code&gt; inside their own context types, so passing cancellation into handlers requires an extraction step. The patterns for keeping cancellation, timeouts, and values straight across any of these wrappers are in &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-context-cancellation-timeouts/" rel="noopener noreferrer"&gt;Go context.Context Done Right&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeouts nobody sets
&lt;/h2&gt;

&lt;p&gt;Every stack in this comparison leaves server timeouts at zero defaults unless you set them — and zero means no limit. A client that opens a connection and sends nothing holds a goroutine and a file descriptor indefinitely:&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;srv&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;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;              &lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ReadHeaderTimeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&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;ReadTimeout&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;WriteTimeout&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;IdleTimeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="m"&gt;120&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;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ReadHeaderTimeout&lt;/code&gt; is the one to set first — it bounds slowloris-style connection exhaustion without breaking long uploads the way a blanket &lt;code&gt;ReadTimeout&lt;/code&gt; can. &lt;code&gt;WriteTimeout&lt;/code&gt; must cover the slowest legitimate response, which makes it a policy decision tied to your timeout budgets rather than a copy-paste constant. The same discipline applies per framework: Gin, Echo, and Fiber all accept server-level timeout configuration, and none of them enable it by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[New Go HTTP service] --&amp;gt; B{Internal tool, plugin,&amp;lt;br/&amp;gt;or zero-dependency requirement?}
    B -- yes --&amp;gt; C[net/http ServeMux]
    B -- no --&amp;gt; D{Want built-in binding,&amp;lt;br/&amp;gt;validation, big middleware catalog?}
    D -- no --&amp;gt; E[chi + net/http]
    D -- yes --&amp;gt; F{Team convention?}
    F -- error-returning handlers --&amp;gt; G[Echo]
    F -- largest ecosystem, Gin idioms --&amp;gt; H[Gin]
    A --&amp;gt; I{Small responses, extreme throughput,&amp;lt;br/&amp;gt;no HTTP/2, upstream TLS termination?}
    I -- all true --&amp;gt; J[Fiber]
    I -- no --&amp;gt; E&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Internal service, CLI endpoint, plugin, library&lt;/td&gt;
&lt;td&gt;net/http alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Most APIs — structure without framework lock-in&lt;/td&gt;
&lt;td&gt;chi on top of net/http&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public API, larger team, validation and binding out of the box&lt;/td&gt;
&lt;td&gt;Gin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public API, centralized error translation, consistent context API&lt;/td&gt;
&lt;td&gt;Echo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proven routing/serialization bottleneck, tiny JSON, no HTTP/2&lt;/td&gt;
&lt;td&gt;Fiber&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the honest default for a new service in 2026 has to be named: start with &lt;code&gt;net/http&lt;/code&gt;, add chi when middleware or route groups start to ache, and move to Gin or Echo only when binding, validation, or team convention justifies a framework identity. Fiber stays a specialist tool — worth benchmarking against when throughput is proven to be the problem, priced against the compatibility bill it charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating between stacks
&lt;/h2&gt;

&lt;p&gt;Migration cost tracks the handler-signature divide. stdlib ↔ chi is mechanical: handlers keep the same signature, routes move from &lt;code&gt;mux.HandleFunc&lt;/code&gt; patterns to &lt;code&gt;r.Get&lt;/code&gt;, middleware attaches to the chi stack. Gin ↔ Echo is a context-type translation — &lt;code&gt;c.Param&lt;/code&gt;, &lt;code&gt;c.JSON&lt;/code&gt;, and middleware logic port nearly line-for-line. Anything moving to or from Fiber is a rewrite of the HTTP layer, because &lt;code&gt;*http.Request&lt;/code&gt;-based code, &lt;code&gt;net/http&lt;/code&gt; middleware, and h2 assumptions do not survive the fasthttp boundary; the least painful route is keeping business logic in framework-free packages and regenerating the transport layer, which is also the structure that makes the next migration cheap.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture hub — API design, code structure, and integration patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/comparisons/most-popular-go-projects-on-github/" rel="noopener noreferrer"&gt;Top Trending Go Projects on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>dev</category>
      <category>go</category>
    </item>
    <item>
      <title>Best Newsletter Platforms in 2026: Pricing, Hosting and SEO</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 05 Oct 2026 10:38:35 +0000</pubDate>
      <link>https://dev.to/rosgluk/best-newsletter-platforms-in-2026-pricing-hosting-and-seo-46ka</link>
      <guid>https://dev.to/rosgluk/best-newsletter-platforms-in-2026-pricing-hosting-and-seo-46ka</guid>
      <description>&lt;p&gt;Newsletter platforms split into thin email senders and full publishing systems with archives and memberships. That split decides hosting, SEO, and migration more than any editor does.&lt;/p&gt;

&lt;p&gt;This comparison covers Buttondown, EmailOctopus, beehiiv, Kit, Ghost, Substack, MailerLite, Brevo, and Sender. Prices are in USD, reflect the public pricing in October 2026, and normally rise as the mailing list grows on subscriber-based plans.&lt;/p&gt;

&lt;p&gt;Each platform section describes what the product actually does, its pricing tiers, and where the public archive lives. The closing sections cover the three archive ownership models, canonical URLs, sender authentication, and API publishing, because those decisions outlast any single vendor choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform models: email-first, publishing-first, hybrid
&lt;/h2&gt;

&lt;p&gt;Email-first services assume that another system owns the website and the content. Publishing-first services want the newsletter platform itself to become an important public destination. A few deliberately sit between those two models, and the architectural choice carries consequences for SEO, archives, and migration.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Newsletter platforms] --&amp;gt; B[Email-first]
    A --&amp;gt; C[Publishing-first]
    A --&amp;gt; D[Hybrid]

    B --&amp;gt; B1[EmailOctopus]
    B --&amp;gt; B2[Brevo]
    B --&amp;gt; B3[Sender]
    B --&amp;gt; B4[MailerLite]

    C --&amp;gt; C1[Substack]
    C --&amp;gt; C2[beehiiv]
    C --&amp;gt; C3[Ghost]

    D --&amp;gt; D1[Buttondown]
    D --&amp;gt; D2[Kit]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Pricing and features at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Free plan&lt;/th&gt;
&lt;th&gt;Paid entry point&lt;/th&gt;
&lt;th&gt;Public archive&lt;/th&gt;
&lt;th&gt;External canonical support&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Main model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buttondown&lt;/td&gt;
&lt;td&gt;100 subscribers&lt;/td&gt;
&lt;td&gt;From $9/mo&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;Hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EmailOctopus&lt;/td&gt;
&lt;td&gt;2,500 subscribers, 10,000 emails/mo&lt;/td&gt;
&lt;td&gt;$10/mo, or $9/mo annual&lt;/td&gt;
&lt;td&gt;Individual web copies&lt;/td&gt;
&lt;td&gt;No documented per-issue override&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Email-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beehiiv&lt;/td&gt;
&lt;td&gt;2,500 subscribers, unlimited sends&lt;/td&gt;
&lt;td&gt;From $43/mo annual&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited for external-site workflows&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Publishing-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kit&lt;/td&gt;
&lt;td&gt;10,000 subscribers, unlimited broadcasts&lt;/td&gt;
&lt;td&gt;From $33/mo annual&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost(Pro)&lt;/td&gt;
&lt;td&gt;No permanent free hosted plan&lt;/td&gt;
&lt;td&gt;From $18/mo annual&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;Publishing-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substack&lt;/td&gt;
&lt;td&gt;Unlimited free publishing&lt;/td&gt;
&lt;td&gt;10% of paid revenue&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;td&gt;Publishing-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MailerLite&lt;/td&gt;
&lt;td&gt;250 subscribers, 2,500 emails/mo&lt;/td&gt;
&lt;td&gt;From $12/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Email-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brevo&lt;/td&gt;
&lt;td&gt;300 sends/day&lt;/td&gt;
&lt;td&gt;From $9/mo&lt;/td&gt;
&lt;td&gt;Web campaign copies&lt;/td&gt;
&lt;td&gt;No comparable publication archive&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Email-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender&lt;/td&gt;
&lt;td&gt;2,500 subscribers, 15,000 emails/mo&lt;/td&gt;
&lt;td&gt;From $7/mo annual&lt;/td&gt;
&lt;td&gt;Web campaign copies&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Email-first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free allowance is a useful starting point, but it should not dominate the decision. A newsletter with 500 subscribers may be operationally better served by a $10 email platform than by a free system that also wants to become the primary CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buttondown
&lt;/h2&gt;

&lt;p&gt;Buttondown stays focused on email while adding publishing features: Markdown support, custom sending domains, a hosted archive, an API, and external canonical URLs for individual issues. A newsletter can therefore keep a public Buttondown presence while the permanent version lives on the publisher's own site.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Canonical website issue] --&amp;gt; B[Buttondown campaign]
    B --&amp;gt; C[Subscribers]
    B --&amp;gt; D[Buttondown archive]
    D --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Buttondown pricing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan or tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Limits / notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;First 100 subscribers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Up to 1,000 subscribers&lt;/td&gt;
&lt;td&gt;$9/mo&lt;/td&gt;
&lt;td&gt;Core newsletter features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Around 5,000 subscribers&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;td&gt;Subscriber-based pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Around 10,000 subscribers&lt;/td&gt;
&lt;td&gt;$79/mo&lt;/td&gt;
&lt;td&gt;Subscriber-based pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Around 20,000 subscribers&lt;/td&gt;
&lt;td&gt;$139/mo&lt;/td&gt;
&lt;td&gt;Subscriber-based pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tagging and segmentation&lt;/td&gt;
&lt;td&gt;+$9/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;+$9/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS-to-email&lt;/td&gt;
&lt;td&gt;+$9/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid subscriptions&lt;/td&gt;
&lt;td&gt;+$9/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple newsletters&lt;/td&gt;
&lt;td&gt;+$29/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automations&lt;/td&gt;
&lt;td&gt;+$29/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom-domain archive&lt;/td&gt;
&lt;td&gt;+$29/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whitelabeling&lt;/td&gt;
&lt;td&gt;+$79/mo&lt;/td&gt;
&lt;td&gt;Optional add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The modular pricing keeps a simple newsletter inexpensive, but stacking several add-ons raises the monthly cost quickly. Buttondown fits publishers whose website remains the authoritative copy and who want distribution plus a secondary public archive rather than a replacement for the website.&lt;/p&gt;

&lt;h2&gt;
  
  
  EmailOctopus
&lt;/h2&gt;

&lt;p&gt;EmailOctopus takes the conventional email-marketing approach: subscriber management, forms, campaigns, templates, automations, analytics, and API access, without trying to become a publishing destination. For a website that already has its own CMS or static-site generator, that is usually exactly what is required.&lt;/p&gt;

&lt;p&gt;Each sent campaign receives a public web version, but EmailOctopus does not automatically build a publication-style archive containing every issue. Its normal model is for the publisher to create the archive page separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  EmailOctopus pricing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Subscribers&lt;/th&gt;
&lt;th&gt;Email allowance&lt;/th&gt;
&lt;th&gt;Notable differences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;2,500&lt;/td&gt;
&lt;td&gt;10,000/mo&lt;/td&gt;
&lt;td&gt;EmailOctopus branding, reports retained 30 days, 1 form and 1 landing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro monthly&lt;/td&gt;
&lt;td&gt;$10/mo&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Branding removed, permanent reports, unlimited forms and landing pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro annual&lt;/td&gt;
&lt;td&gt;From $9/mo&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Same Pro features, 10% annual discount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect Starter&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;2,500&lt;/td&gt;
&lt;td&gt;Unlimited via own SES account&lt;/td&gt;
&lt;td&gt;Amazon SES delivery, EmailOctopus branding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect Pro&lt;/td&gt;
&lt;td&gt;From $8/mo annual&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Unlimited via own SES account&lt;/td&gt;
&lt;td&gt;Full design control and permanent reports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;EmailOctopus Connect is the option for users already operating AWS infrastructure: EmailOctopus manages contacts and campaigns, while Amazon SES performs the actual delivery.&lt;/p&gt;

&lt;p&gt;A sensible website-first archive structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com/newsletter/
    2026-08/
    2026-09/
    2026-10/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with EmailOctopus responsible for sending those issues to subscribers. The hosted web copies are useful for "view in browser" links and sharing, but the permanent archive does not need to depend on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  beehiiv
&lt;/h2&gt;

&lt;p&gt;beehiiv treats newsletters as a publishing business rather than an email list. It combines email, a hosted website, public posts, subscription forms, recommendations, analytics, advertisements, paid subscriptions, digital products, and growth tooling, and the free tier already includes a public website with unlimited email sends.&lt;/p&gt;

&lt;p&gt;That positioning makes beehiiv attractive when the newsletter itself is the publication. For an established publication with its own CMS, the same breadth creates overlap with the existing content system.&lt;/p&gt;

&lt;h3&gt;
  
  
  beehiiv pricing
&lt;/h3&gt;

&lt;p&gt;beehiiv was in the middle of a plan-name transition in October 2026, so customers may see the newer Free/Lite/Pro naming or the previous Launch/Scale/Max terminology depending on account and renewal state.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Subscriber capacity&lt;/th&gt;
&lt;th&gt;Notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Launch / Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Up to 2,500&lt;/td&gt;
&lt;td&gt;Unlimited sends, website, custom domain, analytics, recommendations, API access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;From $49/mo monthly or $43/mo annual&lt;/td&gt;
&lt;td&gt;Tier-based, up to 100,000&lt;/td&gt;
&lt;td&gt;Automations, Ad Network, paid subscriptions, digital products, advanced analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max&lt;/td&gt;
&lt;td&gt;From $109/mo monthly or $96/mo annual&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Branding removal, RSS-to-send, dynamic content, more publications, priority support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;100,000+ / custom&lt;/td&gt;
&lt;td&gt;SSO, dedicated infrastructure options, account management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Email sending is unlimited across the normal newsletter plans, so costs are driven mainly by subscriber count and feature tier rather than campaign volume. beehiiv charges a 0% platform fee on paid subscriptions on the relevant paid plans, although normal payment-processing fees still apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kit
&lt;/h2&gt;

&lt;p&gt;Kit, formerly ConvertKit, sits between creator marketing software and newsletter publishing. Its free tier is unusually generous: up to 10,000 subscribers with unlimited email broadcasts, landing pages, forms, tagging, segmentation, and API access, designed to keep audience growth inexpensive. Paid plans add more sophisticated automation and analytics.&lt;/p&gt;

&lt;p&gt;Kit also provides public newsletter pages, which lets broadcasts function as web content rather than existing only inside inboxes. It makes the most sense when email subscribers are a core business asset and features such as sequences, segmentation, products, and creator-oriented automation matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kit pricing
&lt;/h3&gt;

&lt;p&gt;The prices below use the 1,000-subscriber tier and annual billing for the paid plans.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Subscribers&lt;/th&gt;
&lt;th&gt;Notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Up to 10,000&lt;/td&gt;
&lt;td&gt;Unlimited broadcasts, forms and landing pages, tagging, segmentation, API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creator&lt;/td&gt;
&lt;td&gt;$33/mo annual&lt;/td&gt;
&lt;td&gt;1,000 at quoted tier&lt;/td&gt;
&lt;td&gt;Visual automations, sequences, integrations, RSS campaigns, branding removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creator monthly equivalent&lt;/td&gt;
&lt;td&gt;About $39/mo&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Same feature tier without annual discount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$66/mo annual&lt;/td&gt;
&lt;td&gt;1,000 at quoted tier&lt;/td&gt;
&lt;td&gt;Advanced analytics, subscriber scoring, referral system, unlimited users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro monthly equivalent&lt;/td&gt;
&lt;td&gt;About $79/mo&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Same feature tier without annual discount&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kit's paid prices increase with subscriber count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ghost
&lt;/h2&gt;

&lt;p&gt;Ghost is a complete open-source publishing platform with newsletters built in: a website, CMS, memberships, newsletters, subscriptions, themes, APIs, and strong SEO controls. It can be self-hosted or purchased as the managed Ghost(Pro) service.&lt;/p&gt;

&lt;p&gt;Unlike most hosted newsletter platforms, self-hosted Ghost is open source, and hosting, database operation, and bulk email delivery then become separate infrastructure costs. Ghost is strong on SEO architecture: posts can specify external canonical URLs, and themes can access that canonical URL directly, which allows a Ghost archive to function as an index to content whose authoritative copy lives elsewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newsletter.example.net/
    Issue A -&amp;gt; example.com/newsletter/issue-a/
    Issue B -&amp;gt; example.com/newsletter/issue-b/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For organizations that need both a CMS and newsletters, Ghost is one of the most complete systems available. Deploying it only to send email, however, is unnecessarily heavy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ghost(Pro) pricing
&lt;/h3&gt;

&lt;p&gt;The prices below are annual-billing equivalents, and Ghost(Pro) pricing also changes with audience size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Included members at entry tier&lt;/th&gt;
&lt;th&gt;Notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$18/mo&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Website, custom domain, newsletter, unlimited sends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publisher&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;Custom themes, paid subscriptions, advanced analytics, integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business&lt;/td&gt;
&lt;td&gt;$199/mo&lt;/td&gt;
&lt;td&gt;Higher limits&lt;/td&gt;
&lt;td&gt;15 staff users, priority support, larger usage limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Quote&lt;/td&gt;
&lt;td&gt;Custom / unlimited&lt;/td&gt;
&lt;td&gt;Dedicated IP, SLA, advanced configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Substack
&lt;/h2&gt;

&lt;p&gt;Substack removes most infrastructure decisions. A publication gets email delivery, public posts, archives, subscriber management, comments, recommendations, community features, and paid subscriptions without a monthly platform charge, so its pricing model is fundamentally different from subscriber-tiered services.&lt;/p&gt;

&lt;p&gt;Substack's public archive is central to the product: the platform expects its own web pages to be part of the publication rather than temporary copies of messages sent from another CMS. That is convenient for newsletter-first publishing, but it gives an external website less control over the public archive and the SEO architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Substack pricing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan / usage&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free publication&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Unlimited free subscribers and publishing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free newsletters&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;No monthly platform fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid subscriptions&lt;/td&gt;
&lt;td&gt;10% of subscription revenue&lt;/td&gt;
&lt;td&gt;Charged by Substack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment processing&lt;/td&gt;
&lt;td&gt;Additional&lt;/td&gt;
&lt;td&gt;Stripe processing charges also apply&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This makes Substack extremely inexpensive for a large free newsletter. The economics change once significant paid subscription revenue is involved: a newsletter producing $10,000 per month in subscription revenue would pay approximately $1,000 per month to Substack before payment-processing fees.&lt;/p&gt;

&lt;h2&gt;
  
  
  MailerLite
&lt;/h2&gt;

&lt;p&gt;MailerLite combines traditional email marketing with websites, landing pages, signup forms, and automation. Its free tier became substantially smaller in June 2026, dropping to 250 active subscribers and 2,500 email sends per month; in return, the free plan includes access to a broader set of design and publishing tools than many basic free newsletter products.&lt;/p&gt;

&lt;p&gt;MailerLite provides web versions of campaigns and a newsletter archive component that displays previous campaigns on a MailerLite-hosted page. That makes its public archive functionality stronger than EmailOctopus, although MailerLite is still primarily an email-marketing platform rather than an independent publishing ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  MailerLite pricing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Subscribers / sending&lt;/th&gt;
&lt;th&gt;Notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;250 subscribers, 2,500 emails/mo&lt;/td&gt;
&lt;td&gt;3 automations, 3 forms, 1 landing page, 1 small website&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comfort&lt;/td&gt;
&lt;td&gt;From $12/mo&lt;/td&gt;
&lt;td&gt;Subscriber-tiered; monthly allowance is 10x tier size&lt;/td&gt;
&lt;td&gt;More templates, dynamic email, auto-resend, preference center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;Subscriber-tiered&lt;/td&gt;
&lt;td&gt;Unlimited sending under fair-use policy&lt;/td&gt;
&lt;td&gt;Higher feature limits and more advanced functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;200,000+ subscribers&lt;/td&gt;
&lt;td&gt;Custom account arrangement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Brevo
&lt;/h2&gt;

&lt;p&gt;Brevo is broader than a newsletter platform. Its product includes marketing email, transactional email, SMS, WhatsApp, automation, web push, sales tools, and customer-data functionality, and a newsletter is only one part of that platform. Its pricing is based primarily on sending volume rather than the number of stored contacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Brevo pricing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Sending / contacts&lt;/th&gt;
&lt;th&gt;Notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;300 emails/day, up to 100,000 stored contacts&lt;/td&gt;
&lt;td&gt;Campaigns, transactional email, basic automation and reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;From $9/mo&lt;/td&gt;
&lt;td&gt;From 5,000 emails/mo&lt;/td&gt;
&lt;td&gt;No daily sending limit; multiple volume tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;From $18/mo&lt;/td&gt;
&lt;td&gt;From 5,000 emails/mo&lt;/td&gt;
&lt;td&gt;Unlimited-contact automation, A/B testing, advanced reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Professional&lt;/td&gt;
&lt;td&gt;From $499/mo&lt;/td&gt;
&lt;td&gt;From 150,000 emails/mo&lt;/td&gt;
&lt;td&gt;Multichannel features, 10 users, advanced analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Enterprise controls and support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free plan's 300-email daily limit is unusual. For a list with 250 subscribers it is perfectly usable; for a newsletter with 1,500 subscribers it becomes inconvenient because one full-list campaign cannot be delivered under the free daily allowance. Brevo becomes considerably more attractive when the same organization also needs application-generated email or multichannel marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sender
&lt;/h2&gt;

&lt;p&gt;Sender competes more directly with EmailOctopus and MailerLite. It offers newsletters, forms, landing pages, automation, segmentation, and transactional email, with one of the more generous free allowances in this group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sender pricing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Subscribers&lt;/th&gt;
&lt;th&gt;Email allowance&lt;/th&gt;
&lt;th&gt;Notable features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free Forever&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;2,500&lt;/td&gt;
&lt;td&gt;15,000/mo&lt;/td&gt;
&lt;td&gt;Automation, forms, landing pages, transactional email; Sender branding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard annual&lt;/td&gt;
&lt;td&gt;From $7/mo&lt;/td&gt;
&lt;td&gt;From 1,000&lt;/td&gt;
&lt;td&gt;Roughly 12x subscriber tier/mo&lt;/td&gt;
&lt;td&gt;Branding removal, SMS, more templates and experiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard monthly&lt;/td&gt;
&lt;td&gt;From about $9/mo&lt;/td&gt;
&lt;td&gt;From 1,000&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;td&gt;Same plan without annual discount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Professional&lt;/td&gt;
&lt;td&gt;From $14/mo annual&lt;/td&gt;
&lt;td&gt;From 1,000&lt;/td&gt;
&lt;td&gt;Roughly 24x subscriber tier/mo&lt;/td&gt;
&lt;td&gt;Advanced automation and reporting, priority support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Enterprise sending infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At the free tier, 2,500 subscribers and 15,000 monthly sends allow approximately six full-list newsletters per month. Sender therefore compares well with EmailOctopus on raw free capacity, with the tradeoff that it has fewer integrations and less of a publication-oriented ecosystem than platforms such as Kit or beehiiv.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public archives: three ownership models
&lt;/h2&gt;

&lt;p&gt;Archive functionality decides what happens to a multi-year back catalogue when the platform changes. Three models cover most cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publication archive
&lt;/h3&gt;

&lt;p&gt;Platforms such as Substack, beehiiv, and Ghost treat each newsletter issue as part of a public publication on the provider's domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider.example/publication/issue-1
provider.example/publication/issue-2
provider.example/publication/issue-3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider effectively becomes both newsletter infrastructure and CMS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosted campaign copies
&lt;/h3&gt;

&lt;p&gt;Email-marketing services usually create a browser-readable version of an email after it has been sent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email-provider.example/campaign/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These URLs are useful for "view in browser" links and sharing. They should not necessarily be treated as the permanent content archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Website-owned archive
&lt;/h3&gt;

&lt;p&gt;A publisher with an existing website can keep the permanent copies itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com/newsletter/issue-1/
example.com/newsletter/issue-2/
example.com/newsletter/issue-3/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The email provider then becomes replaceable infrastructure: changing newsletter vendors does not require changing historical URLs. For established websites this is the cleaner model, and it is the newsletter application of the &lt;a href="https://www.glukhov.org/post/2025/02/posse-publish-on-your-own-site-syndicate-elsewhere/" rel="noopener noreferrer"&gt;POSSE principle&lt;/a&gt;, which favors the copy you control over the copy a platform controls. For a static site, the archive is regular site content, as in the &lt;a href="https://www.glukhov.org/web-infrastructure/hugo/deploy-hugo-s3/" rel="noopener noreferrer"&gt;Hugo deployment to AWS S3&lt;/a&gt; guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical URLs and newsletter SEO
&lt;/h2&gt;

&lt;p&gt;If the complete issue appears both on an independent website and on a provider-hosted page, search engines can see two very similar documents. A canonical URL provides an explicit preferred version:&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;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt;
      &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/newsletter/issue-42/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buttondown and Ghost are particularly strong for this architecture because both provide mechanisms for associating content with an external canonical URL. Other newsletter services tend to assume that either their own hosted page is the publication or that their web version is simply an email mirror.&lt;/p&gt;

&lt;p&gt;This does not mean that every duplicate issue creates an SEO problem: search engines routinely consolidate duplicate or near-duplicate pages, and explicit canonical control simply makes the intended architecture clearer. Once the archive is in place, the &lt;a href="https://www.glukhov.org/web-infrastructure/seo/self-hosted-seo-tools-and-platforms/" rel="noopener noreferrer"&gt;self-hosted SEO tools comparison&lt;/a&gt; covers verification and monitoring, the &lt;a href="https://www.glukhov.org/web-infrastructure/wordpress/wordpress-seo-plugins-compared/" rel="noopener noreferrer"&gt;WordPress SEO plugins comparison&lt;/a&gt; covers the metadata layer for WordPress-based sites, and &lt;a href="https://www.glukhov.org/web-infrastructure/indexing/indexnow/" rel="noopener noreferrer"&gt;IndexNow&lt;/a&gt; can tell search engines that a new issue has been published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom sending domains and sender authentication
&lt;/h2&gt;

&lt;p&gt;A serious newsletter should normally use an address under a domain controlled by the publisher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newsletter@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;examplepublication@gmail.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A custom domain can be authenticated with SPF, DKIM, and DMARC and can remain stable when the newsletter platform changes, so infrastructure can move from one provider to another while readers continue to see the same sender identity. This section covers the sending side; for mailbox hosting on the same domain, see the &lt;a href="https://www.glukhov.org/web-infrastructure/domain-services/email/" rel="noopener noreferrer"&gt;hosted email comparison&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;A practical setup sequence for a new sending domain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose the sending domain, usually the main domain or a subdomain, and note it in the newsletter platform.&lt;/li&gt;
&lt;li&gt;Create the sending address in the platform and record the DKIM selector it publishes.&lt;/li&gt;
&lt;li&gt;Add the DNS records the platform provides: an SPF &lt;code&gt;TXT&lt;/code&gt; record listing the sending service, the DKIM &lt;code&gt;TXT&lt;/code&gt; record under the selector, and a DMARC &lt;code&gt;TXT&lt;/code&gt; record at &lt;code&gt;_dmarc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify that each record resolves before sending:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# SPF: which servers may send for the domain&lt;/span&gt;
   dig TXT example.com +short

   &lt;span class="c"&gt;# DKIM: the key published under the platform's selector&lt;/span&gt;
   dig TXT selector1._domainkey.example.com +short

   &lt;span class="c"&gt;# DMARC: the policy record for the domain&lt;/span&gt;
   dig TXT _dmarc.example.com +short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a record is missing or mistyped, the platform's dashboard usually flags the domain as unverified, and sends either bounce or land in spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  API access and automated publishing
&lt;/h2&gt;

&lt;p&gt;For an occasional newsletter, manually creating a campaign is not a problem. For a technical publication, API access becomes useful because newsletter generation can be integrated into the normal content pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Markdown source] --&amp;gt; B[Static site generator]
    B --&amp;gt; C[Website]
    A --&amp;gt; D[Newsletter build step]
    D --&amp;gt; E[Newsletter API]
    E --&amp;gt; F[Subscribers]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The same source material can generate the permanent page, the newsletter excerpts, the campaign metadata, and the links without repeatedly copying content between systems. The send step is a plain HTTP call; endpoint paths and payload fields differ per platform, but the pattern is the same:&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;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.example.com/v1/campaigns"&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;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$NEWSLETTER_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&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; @issue.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one reason API-oriented platforms such as Buttondown, EmailOctopus, Kit, and Ghost remain attractive even when their visual editors are not the main selling point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the architecture, not the editor
&lt;/h2&gt;

&lt;p&gt;The platform choice reduces to one decision: which system owns the content, and which system merely delivers it. A newsletter without an existing website saves a considerable amount of setup by letting a publishing-first platform own the posts, the archive, and the subscriptions. An established website usually gets a simpler long-term architecture by keeping the permanent archive on its own domain and treating the provider as delivery infrastructure that can be replaced without touching historical URLs.&lt;/p&gt;

&lt;p&gt;Whatever the split, three details are cheap to settle before the first send and expensive to unwind later: a custom sending domain with SPF, DKIM, and DMARC in place, an explicit decision about where each issue's canonical copy lives, and an export path that keeps the subscriber list in a standard format.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://buttondown.com/" rel="noopener noreferrer"&gt;Buttondown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.emailoctopus.com/" rel="noopener noreferrer"&gt;EmailOctopus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.beehiiv.com/" rel="noopener noreferrer"&gt;beehiiv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kit.com/" rel="noopener noreferrer"&gt;Kit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ghost.org/" rel="noopener noreferrer"&gt;Ghost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://substack.com/" rel="noopener noreferrer"&gt;Substack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mailerlite.com/" rel="noopener noreferrer"&gt;MailerLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.brevo.com/" rel="noopener noreferrer"&gt;Brevo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sender.net/" rel="noopener noreferrer"&gt;Sender&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seo</category>
      <category>webhosting</category>
    </item>
  </channel>
</rss>
