<?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: Odilon HUGONNOT</title>
    <description>The latest articles on DEV Community by Odilon HUGONNOT (@ohugonnot).</description>
    <link>https://dev.to/ohugonnot</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%2F3833552%2F48d32eab-68ed-4496-8ba6-f01e32806723.png</url>
      <title>DEV Community: Odilon HUGONNOT</title>
      <link>https://dev.to/ohugonnot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ohugonnot"/>
    <language>en</language>
    <item>
      <title>Wrapping Go errors: where, and mostly why</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/wrapping-go-errors-where-and-mostly-why-1ckc</link>
      <guid>https://dev.to/ohugonnot/wrapping-go-errors-where-and-mostly-why-1ckc</guid>
      <description>&lt;p&gt;Mid-review on an order confirmation mailer, I confidently explained that the right call is to wrap an error once, at the package boundary, not at every internal step. The functions that build the message stay silent, only the public function that sends the email adds useful context. I even quoted Dave Cheney to back it up. Except while researching this article, I found out Dave Cheney changed his own mind since then, and the real rule isn't about where you wrap, it's about what you choose to expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advice I just gave
&lt;/h2&gt;

&lt;p&gt;The code in question looked like this: two internal functions that build an email's content, and two public functions that call them before sending:&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;buildOrderMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tpl&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;loadTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-confirm"&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;Message&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// no wrap, propagate as-is&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;tpl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;mailOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&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;msg&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;buildOrderMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;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;"failed to build message for order %d: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sender&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;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea: wrapping at every layer produces a noisy message like &lt;code&gt;failed to send: failed to build message: failed to load template: open failed&lt;/code&gt;. Nobody reads those intermediate layers in production, only the final message and the root cause matter. Wrapping once, at the right spot, keeps the message readable and adds the context that actually carries value, here the order ID. That's a correct argument. It's just not the whole picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Except Dave Cheney changed his mind
&lt;/h2&gt;

&lt;p&gt;In 2016, Dave Cheney published &lt;em&gt;Don't just check errors, handle them gracefully&lt;/em&gt; and popularized &lt;code&gt;pkg/errors&lt;/code&gt;. His recommendation at the time: wrap at every layer with &lt;code&gt;errors.Wrap&lt;/code&gt;, specifically to build a readable stack of context without a traditional stack trace:&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;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"open failed"&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ReadConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;config&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;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"could not read config"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final message becomes &lt;code&gt;could not read config: open failed: open /path: no such file or directory&lt;/code&gt;. Back then, the argument held: without wrapping at every level, a failure inside &lt;code&gt;authenticate()&lt;/code&gt; bubbles up to the top of the stack showing just "no such file or directory," with no clue where or why.&lt;/p&gt;

&lt;p&gt;Then in 2021, while looking for a maintainer to take over &lt;code&gt;pkg/errors&lt;/code&gt;, Cheney wrote that he no longer wraps his errors at all. His reason: structured logging (&lt;code&gt;slog&lt;/code&gt; fields rather than a concatenated string) carries that debug context better than nested layers of &lt;code&gt;Wrap&lt;/code&gt;. The creator of the pattern ended up finding it redundant with modern tooling.&lt;/p&gt;

&lt;p&gt;What matters here isn't picking a side between "always wrap" and "never wrap." It's understanding why both positions are defensible depending on what you're actually trying to solve: a readable production error message, or an API contract between packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real question: what does %w commit you to
&lt;/h2&gt;

&lt;p&gt;Since Go 1.13, &lt;code&gt;%w&lt;/code&gt; in &lt;code&gt;fmt.Errorf&lt;/code&gt; does more than decorate a message. It makes the inner error inspectable via &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt; from any caller. That means &lt;code&gt;%w&lt;/code&gt; isn't a style choice, it's an API commitment:&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;// %w: exposing the inner error, callers can match on it&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;LookupUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;row&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;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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;"lookup user %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// The caller can now do this, and rely on it staying true over time&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNoRows&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic trap: &lt;code&gt;LookupUser&lt;/code&gt; runs on &lt;code&gt;database/sql&lt;/code&gt; today. The day it switches to an ORM, or a cache gets added in front of the query, &lt;code&gt;sql.ErrNoRows&lt;/code&gt; stops bubbling up. The caller's &lt;code&gt;errors.Is&lt;/code&gt; check silently breaks, no compile error, just a 404 that quietly becomes a 500 in production. The &lt;code&gt;%w&lt;/code&gt; had turned an implementation detail (we use &lt;code&gt;database/sql&lt;/code&gt;) into a public promise.&lt;/p&gt;

&lt;p&gt;Which gives a simple rule straight from the Go 1.13 docs: default to &lt;code&gt;%v&lt;/code&gt;, because it preserves the abstraction and exposes nothing. Reach for &lt;code&gt;%w&lt;/code&gt; only when you consciously decide the caller needs to inspect the inner error, and in that case define your own sentinel rather than leaking a dependency's:&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;var&lt;/span&gt; &lt;span class="n"&gt;ErrUserNotFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;LookupUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;row&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;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNoRows&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;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;"lookup user %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrUserNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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;"lookup user %s: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// internal detail, not exposed&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller now matches on &lt;code&gt;ErrUserNotFound&lt;/code&gt;, a sentinel the package owns. If &lt;code&gt;database/sql&lt;/code&gt; disappears tomorrow, the contract still holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The public/private rule that actually holds up
&lt;/h2&gt;

&lt;p&gt;Which leaves the "where" question. The most useful rule I found while digging comes from Efe Karakus's blog: wrap only at public boundaries, let private functions propagate as-is. His example makes the point well: a file path repeated three times in the error message, because every private layer kept adding its own redundant context.&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;// ❌ Every level wraps, even private functions&lt;/span&gt;
&lt;span class="c"&gt;// "failed to read config from /etc/x: failed to read file from /etc/x: failed to open /etc/x: ..."&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Only public functions wrap&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// private, propagate as-is&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// public, wraps once&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;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;"failed to read config from %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly what the order confirmation mailer was already doing, and that's why the initial advice wasn't wrong. It was just incomplete: it answered "where," not "%w or %v."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm changing in the mailer
&lt;/h2&gt;

&lt;p&gt;Revised version, applying both criteria together: where to wrap (the public boundary), and what to expose (only when the caller needs to act on it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;buildOrderMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tpl&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;loadTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-confirm"&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;Message&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// private, propagate as-is&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;tpl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;mailOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&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;msg&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;buildOrderMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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="c"&gt;// %v: the caller has nothing to match on for a template error,&lt;/span&gt;
        &lt;span class="c"&gt;// it just needs to know which order failed&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;"failed to build message for order %d: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="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="n"&gt;sender&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;msg&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="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrRateLimited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// %w here: the caller MUST be able to tell "retry later"&lt;/span&gt;
            &lt;span class="c"&gt;// apart from a permanent send failure&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;"send order %d: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrRateLimited&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;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;"send order %d: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference comes down to one line: the SMTP rate limit is wrapped with &lt;code&gt;%w&lt;/code&gt; because the caller has a genuinely different action to take (retry with backoff). Everything else goes through &lt;code&gt;%v&lt;/code&gt;, useful context for a human reading logs, without promising anyone that &lt;code&gt;*template.Error&lt;/code&gt; will still be exposed ten versions from now.&lt;/p&gt;

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

&lt;p&gt;The real trap isn't picking the wrong side between "wrap everywhere" and "wrap once." It's treating &lt;code&gt;%w&lt;/code&gt; as a cosmetic message flourish when it's actually a contract clause. Once you ask "am I committing my package to always return this specific error," the position in the code becomes secondary. It also taught me to verify a quote before dropping it in a code review, even when it comes from a name as solid as Dave Cheney's.&lt;/p&gt;

</description>
      <category>go</category>
      <category>errors</category>
      <category>fmterrorf</category>
    </item>
    <item>
      <title>Meta Tags Generator: Title, Open Graph and Twitter Card at Once</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Tue, 28 Jul 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/meta-tags-generator-title-open-graph-and-twitter-card-at-once-4dii</link>
      <guid>https://dev.to/ohugonnot/meta-tags-generator-title-open-graph-and-twitter-card-at-once-4dii</guid>
      <description>&lt;p&gt;The familiar scene: you publish a page, share it on LinkedIn, and the preview shows the wrong title, a blank description, and a random image pulled from somewhere on the page. Or worse, no title tag at all because you forgot the CMS doesn't generate one automatically.&lt;/p&gt;

&lt;p&gt;Meta tags live in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and are invisible on the page. That's exactly why they get forgotten. Yet they're what separates a link someone clicks from a link that looks like a phishing attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers, three sets of rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Search engines&lt;/strong&gt; read &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; (shown as the blue clickable line in results, 50-70 characters) and &lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt; (the grey snippet below it, 150-160 chars). Google can override your description with something it generates itself, but if you leave it blank, it picks the first paragraph it finds — rarely your best writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social platforms&lt;/strong&gt; (Facebook, LinkedIn, WhatsApp, Slack) read Open Graph tags: &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt; and &lt;code&gt;og:url&lt;/code&gt;. Without them, the preview is assembled from whatever the crawler finds first. The ideal OG image is 1200 × 630 px — large enough to be readable in a feed, small enough to load quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Twitter/X&lt;/strong&gt; has its own format, Twitter Cards, with &lt;code&gt;twitter:&lt;/code&gt; prefixed tags. In practice, Twitter falls back to OG tags, but the &lt;code&gt;twitter:card&lt;/code&gt; tag lets you choose between a small thumbnail (&lt;code&gt;summary&lt;/code&gt;) and a full-width image (&lt;code&gt;summary_large_image&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The most common mistakes
&lt;/h2&gt;

&lt;p&gt;A title that's too long: Google truncates past roughly 600 pixels — not characters, because a capital "W" takes more space than a lowercase "i". A title that's too short wastes keyword real estate.&lt;/p&gt;

&lt;p&gt;A generic description copy-pasted across every page: "Welcome to our website. Browse our products and services." The description doesn't directly affect rankings, but it's your only chance to speak to someone who's hovering over your link and hasn't decided to click yet. A generic description loses that conversion.&lt;/p&gt;

&lt;p&gt;A missing OG image: the LinkedIn preview shows a blank square. The link looks broken.&lt;/p&gt;

&lt;p&gt;OG tags in French but not in English, or different content per language: social crawlers don't follow language redirects. They see whatever the server returns for the URL they request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the generator
&lt;/h2&gt;

&lt;p&gt;I built a &lt;a href="https://www.web-developpeur.com/en/tools/meta-tags-generator/" rel="noopener noreferrer"&gt;free meta tags generator&lt;/a&gt; that outputs all three layers at once: SEO, Open Graph and Twitter Card.&lt;/p&gt;

&lt;p&gt;It shows a live preview of how your page will appear in Google (title + description + URL), on Facebook/LinkedIn (image + title + domain), and on Twitter (summary or large image). You see exactly what your visitors will see before you publish anything.&lt;/p&gt;

&lt;p&gt;Character counters turn red when you exceed the limits. A global SEO score (out of 100) checks the basics: title length, description present, canonical URL set, OG image provided, viewport defined.&lt;/p&gt;

&lt;p&gt;The generated code is ready to paste into your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, in the recommended order (charset and viewport first, then OG tags).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.web-developpeur.com/en/tools/meta-tags-generator/" rel="noopener noreferrer"&gt;Open the meta tags generator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A few rules worth keeping
&lt;/h2&gt;

&lt;p&gt;Put your main keyword early in the title, ideally in the first three words. The description doesn't directly affect Google rankings, but it drives click-through rate, which feeds back into position over time.&lt;/p&gt;

&lt;p&gt;The canonical tag (&lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt;) prevents duplicate content when a page is accessible from multiple URLs: with and without &lt;code&gt;www&lt;/code&gt;, with and without a trailing slash, with UTM parameters.&lt;/p&gt;

&lt;p&gt;To validate after publishing: the &lt;strong&gt;Facebook Sharing Debugger&lt;/strong&gt; (&lt;code&gt;developers.facebook.com/tools/debug&lt;/code&gt;), the &lt;strong&gt;Twitter Card Validator&lt;/strong&gt;, and &lt;strong&gt;Google Rich Results Test&lt;/strong&gt;. The generator lets you skip the publish-test-fix-republish loop by showing the result upfront.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>metatags</category>
      <category>opengraph</category>
      <category>tools</category>
    </item>
    <item>
      <title>GOMAXPROCS and Kubernetes: Go App Throttled, How to Fix It</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Mon, 27 Jul 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/gomaxprocs-and-kubernetes-go-app-throttled-how-to-fix-it-li6</link>
      <guid>https://dev.to/ohugonnot/gomaxprocs-and-kubernetes-go-app-throttled-how-to-fix-it-li6</guid>
      <description>&lt;p&gt;The Go pod is running in production. CPU limit set to 2, metrics look reasonable. But under load, P99 latencies spike intermittently with no obvious cause. No errors, no goroutine leaks, just latency blowing up on traffic bursts.&lt;/p&gt;

&lt;p&gt;The root cause is usually invisible: &lt;code&gt;GOMAXPROCS&lt;/code&gt; equals the number of CPUs on the physical node, not the container limit. Your Go app thinks it has 32 CPUs when it only has 2. The Linux kernel handles the gap in its own way — CFS throttling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GOMAXPROCS reads (and what it ignores)
&lt;/h2&gt;

&lt;p&gt;By default, the Go runtime computes &lt;code&gt;GOMAXPROCS&lt;/code&gt; via &lt;code&gt;runtime.NumCPU()&lt;/code&gt;, which reads the number of CPUs available at the OS level. On a 32-core Kubernetes node, that returns 32 — regardless of what &lt;code&gt;resources.limits.cpu&lt;/code&gt; says in your pod spec.&lt;/p&gt;

&lt;p&gt;Kubernetes CPU limits are enforced through Linux &lt;strong&gt;cgroups&lt;/strong&gt; (v1 or v2). Cgroups are transparent to processes: a pod with &lt;code&gt;limits.cpu: "2"&lt;/code&gt; doesn't see two virtual CPUs, it sees all the node's CPUs and gets suspended when it consumes too much. The Go runtime, historically, never read cgroups. It trusted the physical core count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"runtime"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Inside a pod with limits.cpu: "2" on a 32-core node&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;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumCPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;      &lt;span class="c"&gt;// → 32&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;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// → 32&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CFS throttling: how the kernel slows you down
&lt;/h2&gt;

&lt;p&gt;The Linux CFS (Completely Fair Scheduler) enforces CPU limits via two cgroup parameters: &lt;code&gt;cpu.cfs_quota_us&lt;/code&gt; (allowed CPU time) and &lt;code&gt;cpu.cfs_period_us&lt;/code&gt; (measurement window, 100 ms by default). A pod limited to 2 CPUs gets at most 200 ms of CPU time per 100 ms window.&lt;/p&gt;

&lt;p&gt;When Go spawns 32 OS threads for 32 parallel goroutines, those threads compete for physical CPUs. Once their combined usage exceeds the cgroup quota within the current window, the kernel suspends all threads in the cgroup until the next window starts. That's throttling: a complete application freeze lasting anywhere from a few milliseconds to several tens of milliseconds. A handful of these per second is enough to tank your P99.&lt;/p&gt;

&lt;p&gt;The signal shows up in Kubernetes Prometheus metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;container_cpu_cfs_throttled_periods_total
container_cpu_cfs_periods_total

&lt;span class="c"&gt;# Throttling ratio (should be near 0)&lt;/span&gt;
rate&lt;span class="o"&gt;(&lt;/span&gt;container_cpu_cfs_throttled_periods_total[5m]&lt;span class="o"&gt;)&lt;/span&gt; /
rate&lt;span class="o"&gt;(&lt;/span&gt;container_cpu_cfs_periods_total[5m]&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The fix: automaxprocs (and Go 1.25)
&lt;/h2&gt;

&lt;p&gt;Uber published &lt;code&gt;go.uber.org/automaxprocs&lt;/code&gt; exactly for this. The package reads the cgroup at startup (v1 or v2), computes the effective quota by dividing &lt;code&gt;quota_us / period_us&lt;/code&gt;, and calls &lt;code&gt;runtime.GOMAXPROCS(n)&lt;/code&gt; with the right value.&lt;/p&gt;

&lt;p&gt;Integration is a single blank import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"runtime"&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"go.uber.org/automaxprocs"&lt;/span&gt; &lt;span class="c"&gt;// reads cgroup in init()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Inside a pod with limits.cpu: "2"&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;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// → 2&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 shell"&gt;&lt;code&gt;go get go.uber.org/automaxprocs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather avoid the dependency and know the value at deploy time, set it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"runtime"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// match your limits.cpu&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downside of the manual approach: change the pod spec and forget to update the code, and you're back to square one. With &lt;code&gt;automaxprocs&lt;/code&gt;, the value tracks the cgroup automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go 1.25: fixed in the runtime itself
&lt;/h3&gt;

&lt;p&gt;Since Go 1.25 (August 2025), the runtime reads cgroups by default on Linux. If the process runs inside a container with a CPU quota lower than the node's core count, &lt;code&gt;GOMAXPROCS&lt;/code&gt; is adjusted automatically — no external dependency needed. Go 1.25 also watches for quota changes at runtime, which matters if Kubernetes adjusts resource limits on a live pod.&lt;/p&gt;

&lt;p&gt;Two GODEBUG options let you opt out if needed:&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;# Ignore cgroup quota (pre-1.25 behavior)&lt;/span&gt;
&lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;containermaxprocs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="c"&gt;# Keep the initial value, skip dynamic updates&lt;/span&gt;
&lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;updatemaxprocs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Go 1.25 and above, &lt;code&gt;automaxprocs&lt;/code&gt; is no longer needed. On Go 1.24 and below, Uber's package remains the standard solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it's working
&lt;/h2&gt;

&lt;p&gt;To check the effective value inside a running pod:&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;# Open a shell in the pod&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; /bin/sh

&lt;span class="c"&gt;# Read the cgroup v2 quota&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/cpu.max
&lt;span class="c"&gt;# → 200000 100000  (= 2 CPUs: 200ms quota / 100ms period)&lt;/span&gt;

&lt;span class="c"&gt;# Read cgroup v1 values&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/cpu/cpu.cfs_quota_us
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/cpu/cpu.cfs_period_us
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you expose Go runtime metrics via Prometheus, the gauge &lt;code&gt;go_sched_gomaxprocs_threads&lt;/code&gt; (available with the &lt;code&gt;runtime/metrics&lt;/code&gt; collector since Go 1.17) shows the current value. It should match your CPU quota, not the node's core count.&lt;/p&gt;

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

&lt;p&gt;CFS throttling in Go Kubernetes workloads is a silent problem: no error logs, no obvious cause, just P99 latency spikes under load. The root cause is almost always the same — &lt;code&gt;GOMAXPROCS&lt;/code&gt; sized to the node, not the container.&lt;/p&gt;

&lt;p&gt;The fix is proportionate to the problem: one import or one runtime flag. On Go 1.25, it's handled by upgrading. On older versions, &lt;code&gt;automaxprocs&lt;/code&gt; has been battle-tested at Uber scale for years. Either way, it's a five-minute change with a measurable effect the next time your traffic spikes.&lt;/p&gt;

</description>
      <category>go</category>
      <category>kubernetes</category>
      <category>performance</category>
      <category>goroutines</category>
    </item>
    <item>
      <title>Go 1.25 testing/synctest: No More Flaky Concurrent Tests</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 26 Jul 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/go-125-testingsynctest-no-more-flaky-concurrent-tests-1h90</link>
      <guid>https://dev.to/ohugonnot/go-125-testingsynctest-no-more-flaky-concurrent-tests-1h90</guid>
      <description>&lt;p&gt;Three green tests locally, two red on CI. The outcome depends on how busy the build server is that day. This isn't a logic bug — it's a &lt;code&gt;time.Sleep&lt;/code&gt; that assumes 100&amp;nbsp;ms is always enough for a goroutine to finish.&lt;/p&gt;

&lt;p&gt;That test doesn't verify anything. It gambles.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;testing/synctest&lt;/code&gt;, stable since Go&amp;nbsp;1.25 (August 2025), solves this at the root. No more guesswork, no more arbitrary waits: the clock advances on demand, inside an isolated bubble.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that lies
&lt;/h2&gt;

&lt;p&gt;Debounce is a classic example of time-dependent concurrent code: trigger an action only if no call has happened in the last N milliseconds. Here's a simple implementation.&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;Debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&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;delay&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;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&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;Timer&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mu&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;Mutex&lt;/span&gt;
    &lt;span class="k"&gt;return&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;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&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;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&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;timer&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;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;timer&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;AfterFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the naive test that goes with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ✗ Fragile&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestDebounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;called&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;debounced&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;called&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="m"&gt;50&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;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;debounced&lt;/span&gt;&lt;span class="p"&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;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&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;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// fingers crossed&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;called&lt;/span&gt; &lt;span class="o"&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"want 1 call, got %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;called&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This passes locally because your machine is fast and 200&amp;nbsp;ms feels like plenty. On a loaded CI runner, the &lt;code&gt;time.AfterFunc&lt;/code&gt; goroutine may not have had a chance to run by the time &lt;code&gt;called&lt;/code&gt; is read. Flaky. No error message explains why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go test -race&lt;/code&gt; won't catch this either. There's no concurrent memory access at the same instant — the problem is purely temporal. Two different tools, two different failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What testing/synctest does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;testing/synctest&lt;/code&gt; runs your test inside a "bubble". Inside the bubble, the &lt;code&gt;time&lt;/code&gt; package uses a fake clock controlled by the test. That clock doesn't tick on its own: it only advances when &lt;strong&gt;all goroutines in the bubble are durably blocked&lt;/strong&gt; — waiting on a channel, a mutex, a timer — but not on a syscall or network I/O.&lt;/p&gt;

&lt;p&gt;The Go&amp;nbsp;1.25 API has exactly two functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;synctest.Test(t, f)&lt;/code&gt; — runs &lt;code&gt;f&lt;/code&gt; in a new bubble and waits for every goroutine in that bubble to exit before returning to the test framework.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;synctest.Wait()&lt;/code&gt; — blocks until all other goroutines in the bubble are durably blocked. The explicit synchronization point: "background work is done for now."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go&amp;nbsp;1.24 had an experimental preview behind &lt;code&gt;GOEXPERIMENT=synctest&lt;/code&gt; with a slightly different API (&lt;code&gt;synctest.Run()&lt;/code&gt; instead of &lt;code&gt;synctest.Test()&lt;/code&gt;). Go&amp;nbsp;1.25 graduated the package to the standard library and finalized the API. No flags, no build tags — direct import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;The same debounce test, rewritten with &lt;code&gt;testing/synctest&lt;/code&gt;:&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;// ✓ Deterministic&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"testing/synctest"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestDebounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;synctest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&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="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;called&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;debounced&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;called&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="m"&gt;50&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;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c"&gt;// Advances the virtual clock by 100 ms instantly.&lt;/span&gt;
        &lt;span class="c"&gt;// The AfterFunc goroutine (fires at 50 ms) wakes up along the way.&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;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&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;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;synctest&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;called&lt;/span&gt; &lt;span class="o"&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"want 1 call, got %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;called&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens inside the bubble:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The three &lt;code&gt;debounced()&lt;/code&gt; calls each schedule a &lt;code&gt;time.AfterFunc&lt;/code&gt; at 50&amp;nbsp;ms, canceling the previous one.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;time.Sleep(100ms)&lt;/code&gt; blocks the test goroutine. All goroutines in the bubble are now durably blocked, so the runtime advances the virtual clock to 50&amp;nbsp;ms. The &lt;code&gt;AfterFunc&lt;/code&gt; goroutine wakes up and runs &lt;code&gt;called++&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; The clock continues to 100&amp;nbsp;ms. The test goroutine wakes up. &lt;code&gt;synctest.Wait()&lt;/code&gt; confirms the &lt;code&gt;AfterFunc&lt;/code&gt; goroutine has finished.&lt;/li&gt;
&lt;li&gt; The assertion on &lt;code&gt;called&lt;/code&gt; is reliable. Always. No gambling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Actual wall-clock time for the test: microseconds, not 200&amp;nbsp;ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What synctest doesn't cover
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;testing/synctest&lt;/code&gt; isn't a silver bullet. Three categories slip through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network I/O.&lt;/strong&gt; A goroutine blocked on a network read isn't "durably blocked" in synctest's terms — an external process can unblock it at any time. If your code makes real HTTP calls, &lt;code&gt;synctest.Wait()&lt;/code&gt; can't reason about its state. Fix: replace the network layer with an interface backed by a deterministic test implementation (&lt;code&gt;net.Pipe()&lt;/code&gt;, &lt;code&gt;httptest.Server&lt;/code&gt;, or a hand-rolled mock).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Syscalls and cgo.&lt;/strong&gt; Same reason: the Go runtime doesn't control what the OS or C code does in the background. These calls are not considered durably blocking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Package-level &lt;code&gt;sync.WaitGroup&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;var wg sync.WaitGroup&lt;/code&gt;). These can't be associated with a bubble. Declare them inside the test function instead.&lt;/p&gt;

&lt;p&gt;Ideal use cases: timers, tickers, debounce, rate limiters, TTL caches, retry with backoff — anything that chains &lt;code&gt;time.After&lt;/code&gt;, &lt;code&gt;time.Sleep&lt;/code&gt;, or &lt;code&gt;time.AfterFunc&lt;/code&gt; with goroutines communicating over channels.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;go test -race&lt;/code&gt; remains the right tool for real data races. &lt;code&gt;testing/synctest&lt;/code&gt; targets a different class of failure: tests whose reliability depends on wall-clock timing. The two tools are orthogonal — use both.&lt;/p&gt;

&lt;p&gt;If you have &lt;code&gt;time.Sleep&lt;/code&gt; in your Go tests to "give the goroutine time to finish," that's the signal. &lt;code&gt;testing/synctest&lt;/code&gt; will do the job faster and correctly. Go&amp;nbsp;1.25 has been out since August 2025 — no flags, no configuration, just import it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📚 Related reading.&lt;/strong&gt; If goroutines that never exit are your problem, the natural follow-up is &lt;a href="https://www.web-developpeur.com/en/blog/goroutine-leaks-golang" rel="noopener noreferrer"&gt;Goroutine leaks in Go: detect, understand, fix&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>concurrency</category>
      <category>goroutines</category>
    </item>
    <item>
      <title>39,000 Torrents: The Bug My Green Benchmark Never Caught</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sat, 25 Jul 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/39000-torrents-the-bug-my-green-benchmark-never-caught-2im6</link>
      <guid>https://dev.to/ohugonnot/39000-torrents-the-bug-my-green-benchmark-never-caught-2im6</guid>
      <description>&lt;p&gt;&lt;code&gt;[Torrent911] Inception.2010.TRUEFRENCH.1080p.BluRay.x264-YIFY&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's a real filename. The goal: extract "Inception" and "2010" from it, query the TMDB API (The Movie Database, the open reference for film and TV metadata), and display the right card. Poster, rating, synopsis. Automatically, for a library of a few hundred files.&lt;/p&gt;

&lt;p&gt;Context: a self-hosted PHP media server. Think Plex, but lighter, no cloud, no account. The interface looks vaguely like Netflix. The hard part is automatic recognition. Torrent release names follow no formal standard: &lt;code&gt;[Torrent911]&lt;/code&gt; can come before the title, &lt;code&gt;TRUEFRENCH&lt;/code&gt; can interrupt it, a release group trails at the end. Films have a year. TV shows almost never do. Popular anime have episode numbers with four digits: &lt;code&gt;One.Piece.S01E1164.VOSTFR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had accumulated 39,188 real torrent names over the years: 22,058 films, 17,130 series. Enough to build an honest test bench. I used an AI assistant to replay variants quickly and analyze where the metrics leaked.&lt;/p&gt;

&lt;p&gt;Two things happened. First, a clean data-driven optimization: measure, iterate, stop at the right time. Then, a classic trap: the benchmark was green while a silent bug turned every single TV show into a film match.&lt;/p&gt;

&lt;p&gt;Two different lessons about what data measures — and what it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minefield inside a string
&lt;/h2&gt;

&lt;p&gt;The pipeline has two distinct stages.&lt;/p&gt;

&lt;p&gt;Stage one: extract a clean title and a year from the filename. This is the foundation. If it returns "Inception 1080p BluRay x264" instead of "Inception", TMDB finds nothing useful. Release naming puts the title first and technical metadata after, but the variations are endless: site prefixes (&lt;code&gt;[Torrent911]&lt;/code&gt;), language tags (&lt;code&gt;VOSTFR&lt;/code&gt;, &lt;code&gt;TRUEFRENCH&lt;/code&gt;), codec (&lt;code&gt;x264&lt;/code&gt;, &lt;code&gt;H.265&lt;/code&gt;), resolution (&lt;code&gt;1080p&lt;/code&gt;, &lt;code&gt;4K&lt;/code&gt;), release groups.&lt;/p&gt;

&lt;p&gt;Stage two: query TMDB with the extracted title and pick the right result. A search for "Breaking Bad" returns dozens of results, including films and documentaries with the same name. You have to score and decide.&lt;/p&gt;

&lt;p&gt;Three metrics to measure extraction quality: &lt;strong&gt;cleanliness&lt;/strong&gt; (% of titles with no residual technical tag — a stray &lt;code&gt;1080p&lt;/code&gt;, &lt;code&gt;WEB-DL&lt;/code&gt;, or &lt;code&gt;S01E02&lt;/code&gt; that should have been stripped), &lt;strong&gt;coverage&lt;/strong&gt; (% of non-empty titles), and &lt;strong&gt;year recall&lt;/strong&gt; (how often the year is correctly extracted). Cleanliness is the main metric. A "dirty" title produces false positives or zero results on TMDB. Coverage was already near 100% from V0.&lt;/p&gt;

&lt;h2&gt;
  
  
  V0 → V4: measure before you optimize
&lt;/h2&gt;

&lt;p&gt;The test bench is entirely offline. It replays the extraction algorithm on all 39,188 corpus filenames, computes the metrics, and returns a report in a few seconds. Deterministic, 100% replayable, no network calls. This is where the AI assistant genuinely helps: building the bench, writing the variants, analyzing the 65 remaining leaks to find patterns. Not "the AI optimized the algorithm" — more like "the AI let me iterate five times in one hour".&lt;/p&gt;

&lt;p&gt;Version&lt;/p&gt;

&lt;p&gt;Approach&lt;/p&gt;

&lt;p&gt;Cleanliness&lt;/p&gt;

&lt;p&gt;Leaks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V0&lt;/strong&gt; — naive&lt;/p&gt;

&lt;p&gt;Separator normalization only (&lt;code&gt;.&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; → space)&lt;/p&gt;

&lt;p&gt;10.84%&lt;/p&gt;

&lt;p&gt;34,939&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V1&lt;/strong&gt; — basic&lt;/p&gt;

&lt;p&gt;Strip 8 known tags (&lt;code&gt;1080p&lt;/code&gt;, &lt;code&gt;BluRay&lt;/code&gt;…)&lt;/p&gt;

&lt;p&gt;50.23%&lt;/p&gt;

&lt;p&gt;19,492&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V2&lt;/strong&gt; — cut at 1st tag&lt;/p&gt;

&lt;p&gt;Truncate at the first technical marker found&lt;/p&gt;

&lt;p&gt;99.83%&lt;/p&gt;

&lt;p&gt;65&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V3&lt;/strong&gt; — episode fix&lt;/p&gt;

&lt;p&gt;Extended episode pattern to 3-4 digits (&lt;code&gt;\d{0,4}&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;99.93%&lt;/p&gt;

&lt;p&gt;28&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V4&lt;/strong&gt; — experimental&lt;/p&gt;

&lt;p&gt;Article normalization, bracket-rescue…&lt;/p&gt;

&lt;p&gt;99.93%&lt;/p&gt;

&lt;p&gt;29&lt;/p&gt;

&lt;p&gt;V0 applies basic separator normalization. 10.84% cleanliness, 34,939 leaks. Catastrophic baseline, but that's the honest starting point.&lt;/p&gt;

&lt;p&gt;V1 strips 8 known technical tags. 50%. The corpus has far more than eight conventions. This approach doesn't scale: every site and every release group has its own patterns.&lt;/p&gt;

&lt;p&gt;V2 introduces the central idea: don't try to enumerate all known tags, just &lt;strong&gt;truncate at the first technical marker found&lt;/strong&gt;. Release naming places the title at the start and technical metadata after. Cutting at the first tag brings cleanliness from 10.84% to 99.83%. 65 leaks remain across 39,000 names. This single shift in approach delivers 99% of the total gain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1 — bracket prefixes are stripped first, no enumeration needed:&lt;/span&gt;
&lt;span class="c1"&gt;// [Torrent911], [HorribleSubs], [FW]... one pattern covers them all.&lt;/span&gt;
&lt;span class="nv"&gt;$clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/\[.*?\]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$clean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[._()[\]{}:]+/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$clean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2 — everything after (and including) the first technical marker is discarded.&lt;/span&gt;
&lt;span class="c1"&gt;// This is what takes cleanliness from 10.8% to 99.8%.&lt;/span&gt;
&lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/\b(multi|vff|truefrench|french|vostfr|bluray|web-?dl|hdtv|x264|x265|hevc'&lt;/span&gt;
  &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'|10bit|remux|2160p|1080p|720p|480p|uhd|hdr|dts|aac|ac3|proper|repack'&lt;/span&gt;
  &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'|extended|directors?-?cut|complete|s\d{1,2}e?\d{0,4}|e\d{2,4})\b.*/i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$clean&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// s\d{1,2}e?\d{0,4}: the {0,4} (instead of {0,2}) catches 3-4 digit episodes&lt;/span&gt;
&lt;span class="c1"&gt;// (One Piece S01E1164, Plus Belle La Vie S12E248) — found by analyzing 39,000 torrents.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;V3 fixes a case revealed by analyzing the 65 remaining leaks. Three and four digit episode numbers weren't captured: &lt;code&gt;S01E1164&lt;/code&gt; for One Piece, &lt;code&gt;S12E248&lt;/code&gt; for Plus Belle La Vie. One change in the regex (&lt;code&gt;\d{0,2}&lt;/code&gt; → &lt;code&gt;\d{0,4}&lt;/code&gt;) cuts the leak count in half, from 65 to 28. You can't guess this by looking at ten examples manually. The corpus forces it to appear in the statistics.&lt;/p&gt;

&lt;p&gt;V4 tests more aggressive normalization. Result: 29 leaks — one more than V3. The data says stop. Adding complexity to regress is a no.&lt;/p&gt;

&lt;p&gt;Extracted title cleanliness V0 to V4 — from 10% to 99.9% Extracted title cleanliness (% with no residual technical tag) 10.84% V0 naive 34,939 leaks 50.23% V1 basic cut 19,492 leaks 99.83% V2 cut at 1st tag 65 leaks 99.93% V3 + episode fix 28 leaks 99.93% V4 experimental 29 leaks&lt;/p&gt;

&lt;p&gt;V2 (truncate at the first technical tag) delivers 99% of the gain. V3 halves the remaining leaks with a single regex character change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three lessons from this part.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The big lever was binary. Going from "keep everything" to "cut at the first technical tag": 10.8% → 99.8% cleanliness in one idea. Refinement (V3, V4) comes after, not before.&lt;/p&gt;

&lt;p&gt;Large-scale data finds what the eye can't see. Without 39,000 examples, four-digit episode numbers stay invisible. The corpus forces them into the statistics.&lt;/p&gt;

&lt;p&gt;Know when to stop. V4 is more complex and slightly worse than V3. YAGNI proven by data, not assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  TMDB matching and the premature optimization
&lt;/h2&gt;

&lt;p&gt;Extracting a clean title is half the work. The other half: query TMDB and pick the right result from the candidates.&lt;/p&gt;

&lt;p&gt;The scoring combines several signals. Title similarity counts for 65% (normalized comparison across all title variants). Year gap: 15%. Type coherence (film or TV): 10 to 18% depending on confidence. TMDB popularity (vote count): 12%, so obscure namesakes lose to well-known titles. Above 55: strong match. Between 35 and 55: quarantine for manual review. Below: rejected.&lt;/p&gt;

&lt;p&gt;When the exact query doesn't produce a good result, the algorithm shortens it word by word until the confidence threshold is reached. A title with unusual punctuation may not match directly: we drop words from the end one at a time and keep the longest query that clears the threshold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Releases often append a subtitle or language tag after the actual title.&lt;/span&gt;
&lt;span class="c1"&gt;// Drop words FROM THE END, one at a time; keep the LONGEST query that clears&lt;/span&gt;
&lt;span class="c1"&gt;// the confidence threshold (55). The title is always at the start.&lt;/span&gt;
&lt;span class="nv"&gt;$words&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$minWords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// at most 5 tries&lt;/span&gt;
&lt;span class="nv"&gt;$best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$bestScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$words&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$minWords&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tmdb_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$preferTv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$cand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$preferTv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$bestScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$bestScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$score&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cand&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bestScore&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// reliable match → stop truncating&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles cases like "Avengers: Age of Ultron" (the colon disappears during cleanup) or foreign-language titles that don't match the English filename. Each iteration costs an API call, but avoids zero-match on titles with punctuation or special characters.&lt;/p&gt;

&lt;p&gt;This is where an "optimization" had quietly slipped into the code. The variable &lt;code&gt;$preferTv&lt;/code&gt; was supposed to signal to TMDB to prioritize TV results. To save API calls, I'd set the algorithm to query the "probable" type first (movie by default, unless the filename explicitly looked like a series), and only query tv &lt;em&gt;if there were no candidates at all&lt;/em&gt;. The network test bench showed flattering numbers: 90% match rate for films, 96.7% for series.&lt;/p&gt;

&lt;p&gt;I navigated it in real conditions. Every single TV show matched a film.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Breaking Bad&lt;/strong&gt; → Breaking Bad Wolf (an indie film with the same name)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Game of Thrones&lt;/strong&gt; → Game of Thrones: The Last Watch (an HBO documentary)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Mandalorian&lt;/strong&gt; → The Mandalorian and Grogu (an upcoming film)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;One Piece&lt;/strong&gt; → One Piece: Gold (a feature film)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;0 correct out of 11 series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the benchmark was showing green
&lt;/h2&gt;

&lt;p&gt;The offline bench only measured extraction. Fair enough, that's its scope. It knew nothing about matching.&lt;/p&gt;

&lt;p&gt;The network bench, though, &lt;em&gt;provided&lt;/em&gt; the content type (movie or tv) directly from the known category of each corpus entry. It passed &lt;code&gt;$preferTv = true&lt;/code&gt; for series directly. It never exercised the automatic type detection, because it didn't need to: the right answer was already baked into the request parameters. The auto-detection that ran in production had never been tested. The aggregate metric was green on a path that real navigation never took.&lt;/p&gt;

&lt;p&gt;A benchmark tests what you tell it to test. Its blind spots are silent.&lt;/p&gt;

&lt;p&gt;Before and after the fix: film/TV type detection for TMDB matching BEFORE « Breaking Bad » type = movie (auto-guessed) TMDB: movies only Breaking Bad Wolf ✗ false positive AFTER « Breaking Bad » movie + tv in parallel scoring picks by popularity Breaking Bad (TV series) ✓ correct match&lt;/p&gt;

&lt;p&gt;The "save an API call" optimization removed tv from the circuit. Scoring can't choose between two types if it only sees one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: four levers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Always query both types.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Movie and tv are now queried every time. Scoring decides. Breaking Bad (TV series on TMDB, high popularity) beats any obscure namesake film on popularity score alone. This was the central regression: saving one API call had broken the selection logic. The fix is literally removing the condition that blocked the second type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Folder structure as a signal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parsing the filename to guess film or series is fragile. A show organized in seasons — "Show/Season 1/", "Show/01/", "Show/02/" — may have no &lt;code&gt;S01E01&lt;/code&gt; in its folder name at all. The folder structure doesn't lie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A folder is a SERIES if it has multiple episodes (≥ 2 videos at top level)&lt;/span&gt;
&lt;span class="c1"&gt;// OR numbered season subdirectories (Season 1, Saison 3, 01, 02…).&lt;/span&gt;
&lt;span class="c1"&gt;// A film has a single video file. Folder SHAPE beats parsing the filename.&lt;/span&gt;
&lt;span class="nv"&gt;$videoCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$seasonish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;scandir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$folder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$folder&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="s2"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^(s\d{1,2}\b|saison|season|saga|arc|part|\d{1,3}$)/i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nv"&gt;$seasonish&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;pathinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PATHINFO_EXTENSION&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nv"&gt;$VIDEO_EXTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$videoCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$isSeries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$videoCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$seasonish&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This signal is used to weight the type coherence bonus in the score. It doesn't replace querying both types; it refines the decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multilingual title merging.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TMDB returns multiple title variants per result: original, local, alternative. When you search for "Attack on Titan", TMDB may return "L'Attaque des Titans" (French) and "Shingeki no Kyojin" (Japanese). Deduplication by ID was keeping only the first title encountered: low similarity with the English query, potentially missed match.&lt;/p&gt;

&lt;p&gt;Fix: aggregate all title variants by ID and score against the best one. Unexpected bonus: Spirited Away went from a weak match to a strong match, because "Sen to Chihiro no Kamikakushi" (the Japanese original title) was already in TMDB data and contributed to the similarity score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reinforced type coherence bonus.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the folder structure clearly confirms a series (numbered subdirectories, dozens of video files), the tv type bonus in the score goes from 10% to 18%. The structural signal amplifies the scoring decision without overriding it.&lt;/p&gt;

&lt;p&gt;Result: 11 series and anime matched correctly at high confidence, 18 films stay films. 0 → 11 on series.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full source code&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
ShareBox is open source. The algorithm described in this article lives in &lt;a href="https://github.com/ohugonnot/sharebox/blob/master/functions.php" rel="noopener noreferrer"&gt;functions.php&lt;/a&gt;: &lt;a href="https://github.com/ohugonnot/sharebox/blob/master/functions.php#L178" rel="noopener noreferrer"&gt;&lt;code&gt;extract_title_year()&lt;/code&gt;&lt;/a&gt; (extraction), &lt;a href="https://github.com/ohugonnot/sharebox/blob/master/functions.php#L629" rel="noopener noreferrer"&gt;&lt;code&gt;tmdb_match()&lt;/code&gt;&lt;/a&gt; (word-removal loop), &lt;a href="https://github.com/ohugonnot/sharebox/blob/master/functions.php#L675" rel="noopener noreferrer"&gt;&lt;code&gt;tmdb_score_candidate()&lt;/code&gt;&lt;/a&gt; (scoring).&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The large-scale data brilliantly optimized extraction. From 10.8% to 99.9% cleanliness in three measured iterations, across 39,000 names. The corpus surfaced a blind spot (&lt;code&gt;S01E1164&lt;/code&gt;) no human would test manually, and it said stop when a more complex variant regressed by one unit.&lt;/p&gt;

&lt;p&gt;The matching failed differently. An API cost optimization caused a regression invisible to the aggregate metrics, because the benchmark was feeding itself the right answer. Thirty seconds of real navigation found what a 96.7% success rate had hidden.&lt;/p&gt;

&lt;p&gt;The real lesson: data optimizes brilliantly what it measures. A benchmark measures one path. It doesn't know others exist. The aggregate metric was an honest indicator; it wasn't a complete one. Testing in real conditions stays irreplaceable, even when everything is green.&lt;/p&gt;

</description>
      <category>matching</category>
      <category>algorithms</category>
      <category>benchmark</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Mathematics describes reality with absurd precision. Nobody really knows why.</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/mathematics-describes-reality-with-absurd-precision-nobody-really-knows-why-4a0</link>
      <guid>https://dev.to/ohugonnot/mathematics-describes-reality-with-absurd-precision-nobody-really-knows-why-4a0</guid>
      <description>&lt;p&gt;In 1865, James Clerk Maxwell was reviewing his own equations. They described electricity and magnetism, two phenomena physicists of the time still treated as separate. Combining them, a number fell out of the calculation: the speed at which his theoretical electromagnetic waves would propagate was approximately 310,000 km/s.&lt;/p&gt;

&lt;p&gt;That was also, within experimental uncertainty, the speed of light measured by Fizeau sixteen years earlier.&lt;/p&gt;

&lt;p&gt;Maxwell drew the obvious conclusion: light is an electromagnetic wave. No experiment, no sensor — just symbols on paper. Hertz would confirm it experimentally in 1888, twenty-three years later.&lt;/p&gt;

&lt;p&gt;This kind of moment repeats throughout the history of science with an unsettling regularity. Mathematics developed for entirely abstract reasons ends up describing physical reality with a precision that has no business existing. In 1960, physicist Eugene Wigner gave the phenomenon a name. He called it "the unreasonable effectiveness of mathematics in the natural sciences." And he was honest enough to admit he had no explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Galileo raised the question, without the answer
&lt;/h2&gt;

&lt;p&gt;In 1623, Galileo published &lt;em&gt;Il Saggiatore&lt;/em&gt;, a scientific polemic responding to a Jesuit who disputed his work on comets. In passing, he slipped in an idea that would travel four centuries:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The book of natural philosophy is perpetually open before our eyes, but it is written in characters different from those of our alphabet: triangles, squares, circles, spheres, cones, pyramids and other geometric figures."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is intuition, not proof. Galileo did not demonstrate that nature is mathematical. He sensed it, stated it, and went back to his comets. In 1623, differential equations did not yet exist. Quantum mechanics, mathematical biology — nobody had the tools to verify it.&lt;/p&gt;

&lt;p&gt;What is striking is that he was right without being able to know it. That kind of prophecy draws its value entirely from what came after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list that makes your head spin
&lt;/h2&gt;

&lt;p&gt;Four examples. Any one of them could have passed for coincidence. Together they form a pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maxwell (1865).&lt;/strong&gt; Four equations unifying electricity and magnetism. Combining them, Maxwell derived the speed of an electromagnetic wave: c&amp;nbsp;=&amp;nbsp;1/√(ε₀μ₀) ≈ 300,000 km/s. That matched the speed of light measured by Fizeau in 1849. Maxwell concluded that light is an electromagnetic wave. Hertz confirmed it experimentally in 1888, twenty-three years later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schrödinger (1926).&lt;/strong&gt; The equation describing the evolution of a quantum system predicted the energy levels of the hydrogen atom with a precision the instruments of the time could not yet reach. Troubling detail: the wave function is inherently complex. Not a computational shortcut where you take the real part at the end. Complex numbers are in the foundations of quantum mechanics. Remove them and you have no quantum mechanics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hodgkin and Huxley (1952).&lt;/strong&gt; Five differential equations to model the action potential of a neuron, based on measurements from the giant squid axon. They predicted the exact shape of the electrical spike a neuron produces, including details that the instruments of the time could not yet resolve. The model was right before the equipment existed to verify it. Nobel Prize in physiology, 1963.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lotka and Volterra (1925–1926).&lt;/strong&gt; Alfred Lotka published his equations in 1925 to describe oscillations in chemical reactions. Vito Volterra independently rediscovered them in 1926, prompted by his son-in-law, a biologist trying to explain oscillations of predator and prey fish populations in the Adriatic: World War I had reduced fishing, predators had multiplied, then prey had bounced back. Two people who did not know each other, working on different problems, arriving at the same equations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complex numbers: the most troubling case
&lt;/h2&gt;

&lt;p&gt;In 1545, Gerolamo Cardano published &lt;em&gt;Ars Magna&lt;/em&gt;. Solving cubic equations, he encountered square roots of negative numbers. He manipulated them — it worked algebraically — but called them "subtle as they are useless." In 1572, Rafael Bombelli systematized their rules in &lt;em&gt;Algebra&lt;/em&gt; and showed that you could find real roots of cubic equations by passing through imaginary territory.&lt;/p&gt;

&lt;p&gt;For about three hundred and fifty years, complex numbers were an abstract algebraic tool. Elegant, useful for certain calculations, but with no connection to physical reality. Physicists sometimes used them as a computational shortcut, but "at the end you take the real part" — the physical world stayed real.&lt;/p&gt;

&lt;p&gt;In 1926, Schrödinger wrote his equation. The wave function is intrinsically complex. Not a shortcut: complex numbers are in the foundations of quantum mechanics. Remove them, you have no quantum mechanics.&lt;/p&gt;

&lt;p&gt;Three hundred and eighty-one years between the "useless" invention and the indispensable application.&lt;/p&gt;

&lt;p&gt;Gap between mathematical invention and physical application: complex numbers (381 years), Riemannian geometry (61 years), Maxwell's equations (23 years) MATHEMATICS PHYSICS Complex numbers Cardano, 1545 381 years Quantum mechanics Schrödinger, 1926 Riemannian geometry Riemann, 1854 61 years General relativity Einstein, 1915 Maxwell's equations Maxwell, 1865 23 years Hertz confirmation Hertz, 1888&lt;/p&gt;

&lt;p&gt;Math invented for abstract reasons, decades or centuries before becoming essential in physics.&lt;/p&gt;

&lt;p&gt;Riemannian geometry is worth noting here. In 1854, Bernhard Riemann developed an abstract theory of curved spaces in his Habilitation lecture at Göttingen. No physical application in sight. In 1915, Einstein used it as the language of general relativity. Sixty-one years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wigner names the unease (1960)
&lt;/h2&gt;

&lt;p&gt;Eugene Wigner, a Hungarian-born American physicist, delivered a lecture at NYU in 1959, published the following year in &lt;em&gt;Communications in Pure and Applied Mathematics&lt;/em&gt; under the title "The Unreasonable Effectiveness of Mathematics in the Natural Sciences."&lt;/p&gt;

&lt;p&gt;The paper opens with an anecdote: two former high school classmates run into each other. One became a statistician. He shows the other an article on population trends. The other points to a curve and asks what that symbol represents. "The Gaussian distribution, the bell curve." "But we're in population statistics — what does this have to do with pi?" The statistician shrugs.&lt;/p&gt;

&lt;p&gt;Wigner builds the argument rigorously, example after example, and concludes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The miracle of the appropriateness of the language of mathematics for the formulation of the laws of physics is a wonderful gift which we neither understand nor deserve."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What is remarkable about this text is its honesty. Wigner does not propose an explanation. He names the phenomenon, documents it, and admits he does not understand it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three answers, none satisfying
&lt;/h2&gt;

&lt;p&gt;Since 1960, responses have accumulated. None closes the debate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The universe is mathematics (Tegmark, 2014).&lt;/strong&gt; In &lt;em&gt;Our Mathematical Universe&lt;/em&gt;, physicist Max Tegmark pushes the idea to its limit: physical reality is not &lt;em&gt;described&lt;/em&gt; by a mathematical structure, it &lt;em&gt;is&lt;/em&gt; one. Every consistent mathematical structure exists physically somewhere in a mathematical multiverse. It is elegant. It is also unfalsifiable by construction, which makes it as much a philosophical position as a scientific one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is survivorship bias (Hamming, 1980).&lt;/strong&gt; Richard Hamming, in an article in the &lt;em&gt;American Mathematical Monthly&lt;/em&gt;, turns the argument around: we only notice the math that works. Thousands of mathematical structures are developed without ever finding physical application. We select the mathematics to fit the problem, then present it as a miraculous coincidence. That is a genuine partial refutation. It does not account for complex numbers in quantum mechanics: there, the math does not "fit" the problem — it is the only language in which the problem can be written at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is evolution (anthropic argument).&lt;/strong&gt; Our brain evolved to model the physical world. That the abstract structures it produces should fit that same world is perhaps not so surprising. This is the most sober explanation. It does not account for math developed in an algebraic ivory tower that turns out to be indispensable in physics three centuries later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a genuine mystery (Wigner, Penrose).&lt;/strong&gt; Wigner himself, and Roger Penrose after him in &lt;em&gt;The Road to Reality&lt;/em&gt; (2004), maintain that the correspondence is too precise, too repeated, too deep to be an artifact. There is something here we do not understand. That is not a mystical position — it is honesty about the state of the question.&lt;/p&gt;

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

&lt;p&gt;What struck me digging into this is that the mystery is already in our code.&lt;/p&gt;

&lt;p&gt;Big O is an abstract mathematical structure. Floating point rests on binary mantissas and exponents, and its quirks (&lt;code&gt;0.1 + 0.2 !== 0.3&lt;/code&gt;) come directly from infinite fractions in base 2. Public key cryptography rests on properties of prime numbers that eighteenth-century mathematicians were studying out of pure curiosity. Artificial neural networks are linear algebra on matrices, nothing more.&lt;/p&gt;

&lt;p&gt;We use structures every day whose effectiveness at describing reality has no definitive explanation. We have just learned to stop finding that strange.&lt;/p&gt;

&lt;p&gt;Galileo had the intuition in 1623. Wigner named the problem in 1960. In 2026, the question remains open.&lt;/p&gt;

</description>
      <category>mathematics</category>
      <category>physics</category>
      <category>science</category>
      <category>culture</category>
    </item>
    <item>
      <title>My Claude Code Skills in Production: What the Audit Taught Me</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Thu, 23 Jul 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/my-claude-code-skills-in-production-what-the-audit-taught-me-2eeg</link>
      <guid>https://dev.to/ohugonnot/my-claude-code-skills-in-production-what-the-audit-taught-me-2eeg</guid>
      <description>&lt;p&gt;A Claude Code skill is a Markdown file you drop into &lt;code&gt;~/.claude/skills/&lt;/code&gt;. It describes a complete workflow: when to trigger it, how to execute it step by step, what it should produce. Claude reads it at the right moment and follows it. No manual command, no copy-pasted prompt. You say "publish this article" and the skill chains the checks, image generation, commit, deploy, and LinkedIn draft on its own.&lt;/p&gt;

&lt;p&gt;I have nine in production on this site. Eight work well. One is problematic. And that one is the most interesting to analyze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nine skills, nine domains
&lt;/h2&gt;

&lt;p&gt;To understand what follows, here's what my nine skills do. Each covers a specific domain of my workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;blog-article&lt;/strong&gt;: write and publish an article (FR + EN, OG image, deploy, LinkedIn, dev.to)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;blog-fix&lt;/strong&gt;: fix a typo or bug on an already-published article without relaunching everything&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;fiche-livre&lt;/strong&gt;: read a technical book PDF and produce a complete book review (summary, 5-axis radar, SVG diagrams, bilingual)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;outil-factory&lt;/strong&gt;: create or iterate on a free web tool (/outils/), with radar scoring and Playwright tests&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;veille-debug&lt;/strong&gt;: diagnose bugs in the automated tech watch system (AI article generation, cron, registry)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;vide-contexte&lt;/strong&gt;: extract non-obvious insights from the current session to persistent memory before clearing context&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;deep-research&lt;/strong&gt;: launch a multi-source search with adversarial verification and cited synthesis&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;frontend-design&lt;/strong&gt;: visual direction guide to avoid producing generic Bootstrap output&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;feature-loop&lt;/strong&gt;: manage the full feature cycle (spec, implementation, review, tests, merge)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eight work cleanly. The ninth, &lt;code&gt;feature-loop&lt;/code&gt;, clocks in at 1,002 lines and roughly 25,000 tokens. That's the one a recent audit put under the microscope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a skills audit actually looks at
&lt;/h2&gt;

&lt;p&gt;Auditing a skill means measuring four things: raw length (lines, tokens), structure (body versus on-demand reference files), the density of urgency markers in the text, and what remains visible in long context, when Claude is on iteration 3 of a loaded session.&lt;/p&gt;

&lt;p&gt;On the eight healthy skills, the numbers are clear: between 70 and 257 lines, 1,000 to 7,700 tokens, simple structure. &lt;code&gt;feature-loop&lt;/code&gt; stands alone: 1,002 lines, 25,000 tokens, and text saturated with NON-NEGOTIABLE / NEVER / MANDATORY / LOCKED at every paragraph.&lt;/p&gt;

&lt;p&gt;The diagnosis fits in one sentence: when everything is critical, nothing is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Salience dilution
&lt;/h2&gt;

&lt;p&gt;When Claude loads a skill, it loads the entire body. Everything in there competes for the model's attention. If ten rules are all marked MANDATORY, the model has no way to decide which one to prioritize when there's a conflict or a context constraint. It doesn't arbitrate: it averages. And averaging ten MANDATORY rules gives zero clear priority.&lt;/p&gt;

&lt;p&gt;The invariants that actually change behavior — the ones that break the workflow if forgotten — drown in token micro-optimizations and design justifications. Result: the most important rules are followed with the same reliability as the least important. Which is: not always.&lt;/p&gt;

&lt;p&gt;On the eight healthy skills, critical rules fit in ten lines maximum. The rest is either absent or in reference files loaded at the step that needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent drift in long context
&lt;/h2&gt;

&lt;p&gt;A skill loads at trigger time. But a work session accumulates: file reads, tool outputs, exchanges, iterations. By iteration 3 of a complex feature, &lt;code&gt;feature-loop&lt;/code&gt;'s SKILL.md is buried under thousands of tokens of accumulated context.&lt;/p&gt;

&lt;p&gt;The rules at the top of the file, Claude still sees. The ones on page 8, less so. This isn't a bug. It's the physics of attention: a language model gives more weight to recent tokens and to tokens at the beginning of context. What's in the middle of a 25,000-token file, buried under tool outputs, is structurally less well processed.&lt;/p&gt;

&lt;p&gt;The fix isn't to move everything to the top. It's to extract what doesn't belong in the body: design justifications, complete examples, 50-line bash recipes. Those belong in a &lt;code&gt;references/&lt;/code&gt; file loaded at the exact step that needs them. The skill body stays an execution contract, not an encyclopedia.&lt;/p&gt;

&lt;h2&gt;
  
  
  What distinguishes the skills that hold
&lt;/h2&gt;

&lt;p&gt;The eight healthy skills share two characteristics that aren't obvious at first glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The description is a trigger, not a summary.&lt;/strong&gt; Each skill has a frontmatter with a short description. That's what Claude reads to decide whether to invoke it. A description that summarizes the skill ("this skill creates book reviews") is less effective than one that lists concrete situations ("trigger on: 'book review', 'summarize this book', a book PDF provided"). The first says what the skill is. The second says when to call it — which is the only thing that matters at trigger time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The body trusts the model.&lt;/strong&gt; Short skills don't over-specify. They give the contract (goal, output format, critical steps) and let Claude reason about the rest. An 80-line well-structured skill outperforms a 400-line over-specified one, because every one of those 80 lines has value and the model can hold them all in mind simultaneously. Over-specification is usually codified mistrust. And mistrust in 400 lines produces a skill even Claude can't follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The public repo: six installable skills
&lt;/h2&gt;

&lt;p&gt;I extracted six of my skills into a public repo: &lt;a href="https://github.com/ohugonnot/claude-skills" rel="noopener noreferrer"&gt;github.com/ohugonnot/claude-skills&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Six skills available, organized as a pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;issue-mr&lt;/strong&gt;: turn a vague idea into a well-formed issue, branch, and MR/PR shell&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;feature-loop&lt;/strong&gt;: autonomous quality-gated loop — writer, test-writer, and reviewer are separate agents&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;senior-review&lt;/strong&gt;: senior-level review by dimension (correctness, security, design, tests), blind reviewers&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;branch-wrap-up&lt;/strong&gt;: clean branch close-out (conventional commit, push, memory capture)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;book-distill&lt;/strong&gt;: read a PDF and produce a verified reading note, citations checked word for word&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;vide-contexte&lt;/strong&gt;: extract non-deducible session insights to persistent memory before /clear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Installation via the Claude Code marketplace in one line, or manually with a symlink:&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;# Via marketplace (recommended)&lt;/span&gt;
/plugin marketplace add ohugonnot/claude-skills
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;feature-loop@web-developpeur-skills

&lt;span class="c"&gt;# Or manually&lt;/span&gt;
git clone https://github.com/ohugonnot/claude-skills.git ~/claude-skills
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/claude-skills/plugins/vide-contexte/skills/vide-contexte ~/.claude/skills/vide-contexte
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing these skills changed how I write them. A skill for personal use can lean on implicit context: project conventions, file structure, things I know without writing them. A public skill must work cold, on an unknown project, without a local CLAUDE.md. That constraint forces you to make explicit everything that was tacit. And an explicit skill is a better skill, even for personal use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The search engine that changes discovery
&lt;/h2&gt;

&lt;p&gt;The real problem with skills isn't writing them. It's discovery. If you have twenty skills spread across sub-folders, knowing which one to call for a given need becomes a problem in itself. The natural reflex is to type the skill name, but you still need to remember the name.&lt;/p&gt;

&lt;p&gt;Claude Code now has a keyword-based skill search. Instead of memorizing the exact list, you can search by domain or intent: "something to publish an article", "a research skill", "to clean up context". The engine returns skills whose description matches.&lt;/p&gt;

&lt;p&gt;This mechanism changes how you write descriptions. If that's what gets indexed, the description must contain the words users will type when searching — not the internal terms you use to name the concept. The difference is subtle but real: "clean context before /clear" triggers on "empty the context", "save before clear", "extract the session". A description oriented toward searched usage, not technical definition.&lt;/p&gt;

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

&lt;p&gt;The real lesson from the audit isn't in the list of rules. It's in the gap between &lt;code&gt;feature-loop&lt;/code&gt; and the other eight. A skill that's too long doesn't break because it's unreadable. It breaks because it dilutes: critical rules drown in noise, and the model no longer has a way to know which ones deserve priority attention.&lt;/p&gt;

&lt;p&gt;A skill body isn't an exhaustive source of truth. It's an execution contract. The cleaner it is, the better it holds when context accumulates. Anything not strictly necessary for execution belongs elsewhere: in a reference file, in a script, or nowhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧩 Installable skills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The five public skills are available at &lt;a href="https://github.com/ohugonnot/claude-skills" rel="noopener noreferrer"&gt;github.com/ohugonnot/claude-skills&lt;/a&gt;. For writing patterns from Anthropic's 17 official skills, see &lt;a href="https://www.web-developpeur.com/en/blog/skills-claude-code-patterns-officiels" rel="noopener noreferrer"&gt;The rule vs the practice&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>skills</category>
      <category>ai</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Claude Code's 17 Official Skills: the Rule vs the Practice</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/claude-codes-17-official-skills-the-rule-vs-the-practice-4f15</link>
      <guid>https://dev.to/ohugonnot/claude-codes-17-official-skills-the-rule-vs-the-practice-4f15</guid>
      <description>&lt;p&gt;Anthropic's docs are clear: a SKILL.md should be under 500 lines. Their own &lt;code&gt;docx&lt;/code&gt; skill is 590. I read all 17 of Anthropic's official skills, frontmatter by frontmatter, while building my own marketplace alongside. Every time, the same gap: the docs say one thing, the corpus does another. And the corpus is the one that's right.&lt;/p&gt;

&lt;p&gt;Here's what actually comes out when you read the code instead of the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The description is 90% of the skill
&lt;/h2&gt;

&lt;p&gt;A skill is useless if it doesn't trigger at the right moment. And what decides triggering isn't the body of the SKILL.md, it's its &lt;code&gt;description&lt;/code&gt; in the frontmatter. Claude reads the list of available skills (just name + description) and picks. So all of the "when to use it" must live in the description, not the body.&lt;/p&gt;

&lt;p&gt;The counter-intuitive part, which Anthropic repeats in its own skill-creator: &lt;strong&gt;Claude under-triggers skills&lt;/strong&gt; far more than it over-triggers them. It only consults a skill for a task it can't handle trivially. "Read this PDF" will never fire the PDF skill, even with a perfect description. Hence an explicit instruction: write descriptions that are a little &lt;em&gt;pushy&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You see it in the code: the &lt;code&gt;xlsx&lt;/code&gt; skill says "Use this skill &lt;strong&gt;any time&lt;/strong&gt; a spreadsheet file is the primary input or output", &lt;code&gt;pptx&lt;/code&gt; says "any time a .pptx is involved &lt;strong&gt;in any way&lt;/strong&gt;". No shyness. You push triggering, you don't hold it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 500-line rule nobody follows
&lt;/h2&gt;

&lt;p&gt;"Keep the SKILL.md under 500 lines" is one of the most quoted rules. In practice, &lt;code&gt;docx&lt;/code&gt; is 590 lines, and the docs themselves add "feel free to go longer if needed". The others range from 32 to 404. The truth: 500 is a target, not a law.&lt;/p&gt;

&lt;p&gt;The real principle behind it isn't length, it's the context budget. The SKILL.md body loads on every trigger. If it's big because it holds the essentials, fine, go over. If it's big because it drags along detail that could live elsewhere, that's the fault. Size alone isn't the sin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progressive disclosure: mostly scripts, rarely references
&lt;/h2&gt;

&lt;p&gt;The central skill pattern is staged loading: metadata (name + description) always in context, the body loaded on trigger, and resources (&lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;references/&lt;/code&gt;) only on demand. A script can even execute without being loaded into context.&lt;/p&gt;

&lt;p&gt;Except that in the corpus, &lt;strong&gt;11 skills out of 17 are a single SKILL.md, with no subfolder at all&lt;/strong&gt;. Splitting isn't the norm, it's the exception you reach for when the domain warrants it. And when you do split, it's mostly &lt;code&gt;scripts/&lt;/code&gt;: &lt;code&gt;pdf&lt;/code&gt;, &lt;code&gt;docx&lt;/code&gt;, &lt;code&gt;xlsx&lt;/code&gt;, &lt;code&gt;pptx&lt;/code&gt; are first and foremost script boxes for deterministic operations. The &lt;code&gt;references/&lt;/code&gt; (docs loaded on demand) appear on only two skills. The lesson: pull the deterministic stuff into executable code long before you pull prose into a side file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "NOT for" Anthropic never writes
&lt;/h2&gt;

&lt;p&gt;Writing my own skills, I systematically added a "NOT for X" clause to the description, to avoid false triggers. Reading the official ones, surprise: they almost never do it. Their descriptions list inclusions ("this includes…"), not exclusions.&lt;/p&gt;

&lt;p&gt;The reason fits in one word: their domains don't overlap. The PDF skill and the Excel skill can't be confused, so no exclusion is needed, and they'd rather maximize triggering. My case is different: my skills touch each other (reviewing a diff, shipping a feature, wrapping up a branch are adjacent). The "NOT for" lets me disambiguate between my own skills. So it isn't a universal rule, it's a response to overlap. If your skills don't compete, don't add it: you'd only reduce your triggering, the worst flaw of all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they actually optimize a description
&lt;/h2&gt;

&lt;p&gt;The most instructive part of the official skill-creator is that they don't guess a good description, they measure it. The protocol, unrolled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;20 eval queries&lt;/strong&gt;: 8-10 that should trigger, 8-10 that shouldn't. Realistic and messy, the way a real user would type (file paths, personal context, typos, lowercase).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The negatives are near-misses&lt;/strong&gt;, not obvious ones. "Write a fibonacci function" as a negative for a PDF skill tests nothing. Good negatives share keywords with the skill but need something else.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Each query run 3 times&lt;/strong&gt; for a reliable trigger rate, then an improvement loop over 5 iterations.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You pick the description by its score on a held-out test set&lt;/strong&gt; (60% train, 40% test), so you don't overfit the queries you already know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's an eval pipeline, not a finger in the air. Most people write a description by feel and move on. Anthropic treats it as the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance: the assumption vs the practice
&lt;/h2&gt;

&lt;p&gt;The gap, row by row, across the 17 official skills:&lt;/p&gt;

&lt;p&gt;The common assumption&lt;/p&gt;

&lt;p&gt;What the official corpus does&lt;/p&gt;

&lt;p&gt;A SKILL.md is under 500 lines&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docx&lt;/code&gt; is 590, and the docs add "go longer if needed"&lt;/p&gt;

&lt;p&gt;The description just says when to use the skill&lt;/p&gt;

&lt;p&gt;It must be &lt;em&gt;pushy&lt;/em&gt;: the real risk is under-triggering&lt;/p&gt;

&lt;p&gt;You split into &lt;code&gt;references/&lt;/code&gt; and &lt;code&gt;scripts/&lt;/code&gt; (progressive disclosure)&lt;/p&gt;

&lt;p&gt;11 of 17 skills are a single file; when you split, mostly &lt;code&gt;scripts/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You restrict the description ("NOT for") to avoid false triggers&lt;/p&gt;

&lt;p&gt;Almost never written: distinct domains, they maximize triggering&lt;/p&gt;

&lt;p&gt;A good description is written by feel&lt;/p&gt;

&lt;p&gt;It's measured: 20 queries, each run 3 times, a 5-iteration loop, scored on a held-out test set&lt;/p&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;The real lesson isn't in the list of rules, it's in the gap. The docs give clean heuristics; the corpus shows how competent people bend them when the ground demands it. Reading the 17 skills teaches more than reading the doc page, because you see the real trade-offs: going past 500 lines when the content deserves it, skipping the split two-thirds of the time, pushing triggering rather than restricting it.&lt;/p&gt;

&lt;p&gt;If you keep one thing: the risk isn't that your skill triggers too much, it's that it doesn't trigger at all. Write the description to be found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧩 My skills, installable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built a marketplace of Claude Code skills from my real workflow, applying these patterns. Browse and install them on the &lt;a href="https://www.web-developpeur.com/en/skills" rel="noopener noreferrer"&gt;Skills page&lt;/a&gt;, including a &lt;a href="https://www.web-developpeur.com/en/skills/skill-builder" rel="noopener noreferrer"&gt;skill-builder&lt;/a&gt; skill that condenses these principles. To frame an agent, see also the &lt;a href="https://www.web-developpeur.com/blog/claude-md/" rel="noopener noreferrer"&gt;CLAUDE.md contexts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How many lines should a SKILL.md be?
&lt;/h3&gt;

&lt;p&gt;Aim for under 500, but it's not a law: the official docx skill is 590. What matters is that the body doesn't drag detail that should live in a script or a reference file. Go over if the essential content warrants it, then add pointers to side files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn't my skill trigger?
&lt;/h3&gt;

&lt;p&gt;Almost always the description. Claude under-triggers by default and only consults a skill for a non-trivial task. Make the description more inclusive and "pushy" ("use this skill whenever… even if the user doesn't say…"), with real trigger phrases. And test: a one-step task will never trigger a skill, and that's normal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need references/ and scripts/ folders?
&lt;/h3&gt;

&lt;p&gt;Not by default: 11 of the 17 official skills are a single SKILL.md. You split when the domain is heavy, and then mostly into scripts/ for deterministic work (file manipulation, validation). references/ only earns its place for large multi-variant domains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skill, CLAUDE.md or hook?
&lt;/h3&gt;

&lt;p&gt;Skill = a capability triggered by its description when the situation calls for it. CLAUDE.md = context always loaded for a specific project. Hook = deterministic automation on an event, enforced by the harness, not the model. If it's "always do X", it's a hook or a CLAUDE.md, not a skill.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>skills</category>
      <category>ia</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>Event Sourcing and CQRS: A Practical Guide for Developers</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:00:07 +0000</pubDate>
      <link>https://dev.to/ohugonnot/event-sourcing-and-cqrs-a-practical-guide-for-developers-m6o</link>
      <guid>https://dev.to/ohugonnot/event-sourcing-and-cqrs-a-practical-guide-for-developers-m6o</guid>
      <description>&lt;p&gt;The accountant walks up to your desk. "This account shows 150 dollars. Why?" You open the database, you look at the &lt;code&gt;balance&lt;/code&gt; column. It says 150. That's all it can say. The twenty operations that led to that number? Overwritten, one by one, on every &lt;code&gt;UPDATE&lt;/code&gt;. You can't answer.&lt;/p&gt;

&lt;p&gt;That's the problem Event Sourcing solves. And CQRS is the companion that makes it usable day to day. These two words sound scary, we associate them with Kafka, microservices and unicorn architectures. The reality is simpler, and more useful. This guide lays out the mental model, says when these patterns are worth it (and when they're not), then traces a concrete implementation path in Go with PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Sourcing in one sentence
&lt;/h2&gt;

&lt;p&gt;Instead of storing the current state of a piece of data, you store every event that led to that state. The current state is recomputed by replaying the events in order.&lt;/p&gt;

&lt;p&gt;A bank account is no longer a &lt;code&gt;balance = 150&lt;/code&gt; row. It's a list: &lt;code&gt;Credited(200)&lt;/code&gt;, &lt;code&gt;Debited(70)&lt;/code&gt;, &lt;code&gt;Credited(20)&lt;/code&gt;. The balance is the result of a computation, not a stored value. You've lost nothing, because you never overwrite anything. You only ever append.&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;// CRUD: you store the result. The history is overwritten.&lt;/span&gt;
&lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Event Sourcing: you store the facts. The balance is recomputed.&lt;/span&gt;
&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Credited&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;Debited&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;Credited&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// 150, rebuilt from the start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An event is a past fact, immutable, named in the past tense: &lt;code&gt;OrderPlaced&lt;/code&gt;, &lt;code&gt;PaymentConfirmed&lt;/code&gt;, &lt;code&gt;AccountDebited&lt;/code&gt;. Once written, it never changes. That single decision shapes everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  CQRS in one sentence
&lt;/h2&gt;

&lt;p&gt;You separate write operations (the &lt;em&gt;commands&lt;/em&gt;, which change state) from read operations (the &lt;em&gt;queries&lt;/em&gt;, which read state). Two paths, two models.&lt;/p&gt;

&lt;p&gt;CQRS and Event Sourcing are two independent patterns. You can do CQRS without Event Sourcing, and the other way around. But in production they almost always go together: the write side produces events, and the read side serves from views computed off those events. The write model protects the business rules, the read model is optimized to be read fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three real wins
&lt;/h2&gt;

&lt;p&gt;You don't adopt Event Sourcing to look modern. You adopt it for three concrete things, and if none of them speaks to you, move on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;A free, immutable audit log.&lt;/strong&gt; Every change is a timestamped event you never overwrite. In fintech, in healthcare, anywhere a regulator can ask "who did what, when", this is the argument that justifies everything else.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Temporal queries.&lt;/strong&gt; "What was the balance last Tuesday at 2pm?" becomes trivial: you replay the events up to that date. With a CRUD table, that question is impossible to answer.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Rebuilding.&lt;/strong&gt; A corrupted projection, a bug in a read view? You throw it away and recompute it from the events. The source of truth is intact by construction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to use Event Sourcing
&lt;/h2&gt;

&lt;p&gt;This is the section tutorials forget. Event Sourcing has a real cost: more code, a different mental model, a team to train. For most applications, a plain old CRUD with an audit table is more than enough.&lt;/p&gt;

&lt;p&gt;Avoid Event Sourcing if your domain is a simple form that saves rows, if history doesn't interest you, if nobody will ever ask you "and before?", or if the team is discovering the topic on a project already under pressure. The pattern shines on domains where the history IS the data: accounts, payments, inventory, bookings, complex business workflows. Elsewhere, it adds friction without paying off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete mental model
&lt;/h2&gt;

&lt;p&gt;Three building blocks are enough to understand everything. Once they're clear, the rest is just implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The aggregate&lt;/strong&gt; is the guardian of business consistency. It's the one that says "no" when an operation is invalid (an account doesn't go below zero, an already-shipped order can't be cancelled). In Event Sourcing, the aggregate has no directly persisted state: its state is the result of replaying all its events from the beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The command&lt;/strong&gt; is an intent ("debit this account by 70"). It goes through the aggregate, which validates, and which produces one or more events if it's allowed, or an error if it isn't. Never both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The projection&lt;/strong&gt; is a view computed from the events. "The balance of every account" is a projection. "The list of pending orders" is another. These are what your &lt;em&gt;queries&lt;/em&gt; read, and you can throw them away and recompute them at will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation path, and where to dig deeper
&lt;/h2&gt;

&lt;p&gt;I've written a series of articles that take each block in hand, with real, tested Go code. Here's the order that makes sense for learning, from the mental model to production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The starting point:&lt;/strong&gt; understanding why writes must be serialized per aggregate while reads can be massively parallel, in &lt;a href="https://www.web-developpeur.com/en/blog/concurrence-parallelisme-go-event-sourcing" rel="noopener noreferrer"&gt;concurrency vs parallelism in Go applied to Event Sourcing&lt;/a&gt;. It's also the article that defines the concepts with no prerequisites.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The aggregate:&lt;/strong&gt; the &lt;code&gt;Transition()&lt;/code&gt; function that replays events, and the &lt;code&gt;Clone()&lt;/code&gt; trap Go makes you forget (slices share their backing array and silently corrupt old states), in &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-aggregate-transition-clone" rel="noopener noreferrer"&gt;CQRS in Go: the aggregate, Transition() and Clone()&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The command handler:&lt;/strong&gt; the &lt;code&gt;Handle(ctx, state, cmd) (Events, error)&lt;/code&gt; signature that makes business logic testable without mocks or a database, in &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-command-handlers-testabilite" rel="noopener noreferrer"&gt;side-effect-free command handlers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Storage:&lt;/strong&gt; why PostgreSQL is enough as an event store, with an append-only table and optimistic locking via &lt;code&gt;UNIQUE(aggregate_id, version)&lt;/code&gt;, in &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-postgresql-event-store" rel="noopener noreferrer"&gt;PostgreSQL as an event store&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Multiple aggregates cooperating:&lt;/strong&gt; event choreography over a central orchestrator, and sagas, in &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-go-sagas-choreographie-events" rel="noopener noreferrer"&gt;sagas and event choreography&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Reliability against duplicates:&lt;/strong&gt; the idempotency key for retries and double-clicks, then the four idempotency layers of a complete CQRS system, in &lt;a href="https://www.web-developpeur.com/en/blog/idempotence-cqrs-event-sourcing-bases" rel="noopener noreferrer"&gt;the basics of idempotency&lt;/a&gt; then &lt;a href="https://www.web-developpeur.com/en/blog/idempotence-cqrs-event-sourcing-avance" rel="noopener noreferrer"&gt;commands, projections and outbox&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The advanced case:&lt;/strong&gt; how to return a synchronous result to the HTTP client in an asynchronous system, and keep a consistent audit log, in &lt;a href="https://www.web-developpeur.com/en/blog/cqrs-pubsub-bridge-audit-atomique" rel="noopener noreferrer"&gt;the pubsub bridge and atomic audit&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The minimal stack that's enough
&lt;/h2&gt;

&lt;p&gt;The biggest trap in Event Sourcing isn't the concept, it's over-engineering. You get sold Kafka, EventStoreDB, a distributed message bus and three brokers before you even have a single aggregate that works.&lt;/p&gt;

&lt;p&gt;For 90% of projects, PostgreSQL alone does all the work. An append-only table for events, a &lt;code&gt;UNIQUE&lt;/code&gt; constraint to handle concurrency, polling or &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; for projections. Snapshots and an outbox to a broker only arrive when a real volume or integration problem demands them, not before. Start small. You'll add infrastructure the day the pain is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Event Sourcing and CQRS aren't an architecture you adopt wholesale because it's fashionable. They're tools for a specific problem: when the history of your data matters as much as its current state. If a regulator, an accountant or a production bug might one day ask you "and before?", they're worth their cost.&lt;/p&gt;

&lt;p&gt;The right first step isn't to re-architect everything. It's to take a single aggregate that matters (an account, an order, a wallet), model it as events on a PostgreSQL table, and see what changes. The rest follows, block by block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's the difference between Event Sourcing and CQRS?
&lt;/h3&gt;

&lt;p&gt;Event Sourcing describes how you store data: a sequence of immutable events rather than the current state. CQRS describes how you organize code: one path to write (commands), another to read (queries). They're two independent patterns, but in production you almost always combine them, because one feeds the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you need Kafka or EventStoreDB for Event Sourcing?
&lt;/h3&gt;

&lt;p&gt;No. For the vast majority of projects, PostgreSQL is enough: an append-only table, a uniqueness constraint for concurrency, polling or LISTEN/NOTIFY for projections. Kafka and dedicated event stores answer specific volume or integration needs, not a prerequisite of the pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't Event Sourcing too complex for my project?
&lt;/h3&gt;

&lt;p&gt;Often, yes. If your domain is a simple CRUD with no need for history, a classic audit table is enough and costs far less. Event Sourcing is worth its cost when the history is the data: accounts, payments, inventory, bookings, or any domain where "who did what, when" is a real question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do you start concretely?
&lt;/h3&gt;

&lt;p&gt;With a single aggregate that matters, modeled as events on a PostgreSQL table. Understand replay and the aggregate first, then the command handler, then storage, then idempotency. The linked article series follows exactly that order, with tested Go code.&lt;/p&gt;

</description>
      <category>eventsourcing</category>
      <category>cqrs</category>
      <category>architecture</category>
      <category>go</category>
    </item>
    <item>
      <title>What Science Actually Says About CVs (and 3 HR Myths to Drop)</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/what-science-actually-says-about-cvs-and-3-hr-myths-to-drop-41e0</link>
      <guid>https://dev.to/ohugonnot/what-science-actually-says-about-cvs-and-3-hr-myths-to-drop-41e0</guid>
      <description>&lt;p&gt;My CV had just been redone — clean, two columns, badges, a QR code. I thought it looked great. And that's exactly when the doubt creeps in: great for whom? For me, who built it, or for the recruiter who'll scan it in seven seconds between two meetings?&lt;/p&gt;

&lt;p&gt;I'd followed the usual advice, the kind you read on every HR blog. "75% of CVs are rejected by bots before a human sees them." "CVs with numbers get 40% more callbacks." Sentences I'd swallowed without ever clicking the source. So before patting myself on the back, I wanted to know one simple thing: what does &lt;em&gt;science&lt;/em&gt; actually say about CVs, and what's just folklore copied from blog to blog?&lt;/p&gt;

&lt;p&gt;I pointed AI at it. Not to write my CV — I'd already done that (&lt;a href="https://www.web-developpeur.com/en/blog/refonte-cv-claude-iteration-ats" rel="noopener noreferrer"&gt;36 iterations with Claude, the story is here&lt;/a&gt;). To dig out the real studies, demand primary sources, and flush out the invented numbers. Three research agents in parallel, one non-negotiable rule: no statistic without a verifiable source, and anything that smells like a myth, you flag it. What came back made me throw out half of what I thought I knew.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myths I almost optimized for
&lt;/h2&gt;

&lt;p&gt;The worst thing about bad advice is when it's precise. A round number, a source that sounds serious, and boom, you build your CV around it. Three examples you've definitely read, and they're all hot air.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"75% of CVs are rejected by the ATS before a human."&lt;/strong&gt; False. The line traces back to a 2012 sales pitch by Preptel, a CV-optimization company that shut down in 2013. No study, no methodology, no published data. Ever. The reality, measured by surveying recruiters about their own tools: ATS systems &lt;em&gt;rank&lt;/em&gt; and &lt;em&gt;file&lt;/em&gt; applications, they almost never auto-reject. The only hard filters are knockout questions the recruiter sets by hand (work authorization, location, required certification). The automatic 75% wall doesn't exist.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Optimizing your CV against a rejecting robot means preparing for a fight that isn't happening. The real reader is still a busy human.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;"Numbers get you 40% more callbacks."&lt;/strong&gt; A ghost citation. The supposed source (TalentWorks) is a dead domain, and the original "40%" didn't even mean callbacks: it referred to meeting 40% of a job's requirements. The figure got laundered from blog to blog until it became a truth nobody checked. To be clear, I'm not saying numbers are useless — quite the opposite (more on that below). I'm saying the &lt;em&gt;precise multiplier&lt;/em&gt; is invented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Recruiters read in an F-pattern."&lt;/strong&gt; The famous F-pattern comes from a 2006 Nielsen Norman Group study on reading &lt;em&gt;web pages&lt;/em&gt;, not CVs. The CV extrapolation was made by blogs, never by a study on recruiters. NN/g itself lists five reading patterns and notes the F is "rarely a perfect F." What stays true is that the top and the left draw attention. But "your recruiter reads in an F," nobody has shown that on a CV.&lt;/p&gt;

&lt;p&gt;The common thread across these three myths: a clean number, a fuzzy authority, and zero link to a study. The reflex that saves you is to ask for the primary source. Nine times out of ten, it doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually proven
&lt;/h2&gt;

&lt;p&gt;The good news is there's real science underneath. Less sexy, more nuanced, but usable. Here's what holds up, with the honest level of evidence next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing quality has a measured causal effect.&lt;/strong&gt; This is the strongest evidence of the lot: a controlled experiment by &lt;a href="https://arxiv.org/abs/2301.08083" rel="noopener noreferrer"&gt;van Inwegen, Munyikwa and Horton (2023)&lt;/a&gt; on roughly 480,000 jobseekers shows that receiving writing assistance raised the probability of being hired by 8%. Not the polish — the clarity. Writing better helps the employer &lt;em&gt;assess&lt;/em&gt; your ability, not just guess it. A real randomized trial, large sample. If you keep one thing: sweat the sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two pages, for an experienced profile, is defensible.&lt;/strong&gt; A &lt;a href="https://www.resumego.net/research/one-or-two-page-resumes/" rel="noopener noreferrer"&gt;ResumeGo simulation (2018)&lt;/a&gt; saw recruiters prefer two-page CVs over one-page ones, especially for managerial profiles. Take it with a grain of salt (study funded by a CV-writing service, and it's a simulation, not real hiring), but it lines up with the industry consensus: one page under ten years of experience, two beyond, three almost always penalized outside academia, medicine and law.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The photo isn't neutral.&lt;/strong&gt; A study in &lt;em&gt;Management Science&lt;/em&gt; (&lt;a href="https://pubsonline.informs.org/doi/10.1287/mnsc.2014.1927" rel="noopener noreferrer"&gt;Ruffle &amp;amp; Shtudiner, 2015&lt;/a&gt;, 5,312 CVs sent) shows the photo introduces a measurable attractiveness and gender bias that can backfire on you. In the US and UK, omitting it is the norm and cuts discrimination risk. Verdict: no photo is a defensible choice, not negligence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vocabulary matters more than the "robot."&lt;/strong&gt; Harvard's &lt;a href="https://www.hbs.edu/managing-the-future-of-work/research/Pages/hidden-workers-untapped-talent.aspx" rel="noopener noreferrer"&gt;Hidden Workers study (2021)&lt;/a&gt;, on 8,000+ workers, shows the real problem isn't auto-rejection, it's &lt;em&gt;vocabulary mismatch&lt;/em&gt;: if you don't use the words from the posting, the recruiter searching their database doesn't find you. So yes, mirroring the job's language is worth it. Not to clear a fictional wall — to be findable and readable by the human who scans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the seven-second scan?&lt;/strong&gt; The number comes from a TheLadders eye-tracking study, small (30 recruiters), commercial, never replicated. Treat it as a rhetorical image, not a measurement. But the idea behind it holds: the first second decides, and the top of the CV must instantly say "good profile." That part is solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one real technical trap: columns
&lt;/h2&gt;

&lt;p&gt;Where the ATS myth deflates, a very real problem takes its place: &lt;em&gt;parsing&lt;/em&gt;. When an ATS reads your CV, it turns it into structured text, line by line, left to right. Give it two columns, and it reads across: your job title on the left blends with a skill from the right column, and the output becomes mush.&lt;/p&gt;

&lt;p&gt;Practical tests on five ATS platforms (Workday, Greenhouse, Lever, iCIMS, Taleo) confirm it, with numbers: on a two-column layout, Taleo flat-out lost an entire job and returned an empty skills list. The CSS-styled skill "chips"? Either ignored or shattered into orphan tokens. The QR code? An image, so invisible to the parser. In short, everything that makes my CV pretty to a human eye makes it risky for a machine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My two-column CV is gorgeous for a human and treacherous for a parser. Both are true at the same time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix isn't to butcher the design, it's to keep &lt;strong&gt;two versions&lt;/strong&gt;. The pretty two-column PDF for what you actually do with a freelance CV: send it directly, link it from your site, hand it over in person. And a single-column version, no chips, standard section headings, "Jan 2021" dates, for the corporate portals with an ATS. Free 30-second test: copy-paste your CV into a plain-text editor. If the columns scramble, the ATS sees the same mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The radar: scoring a CV on what actually matters
&lt;/h2&gt;

&lt;p&gt;Once the folklore was set aside, eight criteria with real evidence behind them remained. I turned them into a grid and scored myself with it. No-mercy verdict on my own CV:&lt;/p&gt;

&lt;p&gt;Criterion (evidence-backed)&lt;/p&gt;

&lt;p&gt;Score&lt;/p&gt;

&lt;p&gt;Why&lt;/p&gt;

&lt;p&gt;Quantified impact&lt;/p&gt;

&lt;p&gt;9/10&lt;/p&gt;

&lt;p&gt;Almost every line carries a number (99.9% uptime, integration complexity −80%, response time ÷4).&lt;/p&gt;

&lt;p&gt;Seniority &amp;amp; scope&lt;/p&gt;

&lt;p&gt;8/10&lt;/p&gt;

&lt;p&gt;Dev → Lead → Architect progression visible, explicit architecture decisions. Missing 1-2 "chose X over Y" trade-offs.&lt;/p&gt;

&lt;p&gt;Writing &amp;amp; clarity&lt;/p&gt;

&lt;p&gt;8.5/10&lt;/p&gt;

&lt;p&gt;Action verbs, precise prose. The best-proven axis (the +8% trial).&lt;/p&gt;

&lt;p&gt;Human readability / design&lt;/p&gt;

&lt;p&gt;9/10&lt;/p&gt;

&lt;p&gt;Good-looking, clear hierarchy, scannable in seconds.&lt;/p&gt;

&lt;p&gt;ATS compatibility (parsing)&lt;/p&gt;

&lt;p&gt;4.5/10&lt;/p&gt;

&lt;p&gt;The flip side of the design: two columns + chips + QR break parsing. No flat version.&lt;/p&gt;

&lt;p&gt;Hierarchy / top of CV&lt;/p&gt;

&lt;p&gt;6.5/10&lt;/p&gt;

&lt;p&gt;Experience starts on page 2. The strongest role sits below the fold.&lt;/p&gt;

&lt;p&gt;Length / concision&lt;/p&gt;

&lt;p&gt;6/10&lt;/p&gt;

&lt;p&gt;Three pages. The seniority consensus caps at two.&lt;/p&gt;

&lt;p&gt;Proof / portfolio (GitHub, projects)&lt;/p&gt;

&lt;p&gt;8.5/10&lt;/p&gt;

&lt;p&gt;Annotated projects with stack and substance, GitHub and portfolio linked. A real reason to click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overall: 7.5/10.&lt;/strong&gt; The content is top-tier: quantified, senior, proven. The only two real weaknesses are &lt;em&gt;mechanical&lt;/em&gt;, not substantive: ATS parsing caused by the design, and the length that top-loads badly. Exactly the kind of diagnosis you can't make on your own, because you confuse "I like my CV" with "my CV does the job."&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually changed
&lt;/h2&gt;

&lt;p&gt;Three levers, in order of impact, and none touches the content (which is good):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A flat ATS version.&lt;/strong&gt; Not instead of the pretty PDF: in addition. The two-column PDF for direct sending and the site, a single-column version for corporate portals. It's only a priority if you target big companies with an ATS. For freelance use where you share the link yourself, the human design stays the right call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go to two pages and lift the experience up.&lt;/strong&gt; Compact the last page (merge old education, trim interests) and start the first role on page 1. The recruiter must read "Backend Architect, fintech" in the first seconds, not a wall of profile text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two substantive tweaks.&lt;/strong&gt; Add a real architecture trade-off ("modular monolith over microservices because...") and, when the number exists, a business impact in euros or users. That's what moves you from "senior" to "architect" in a hiring manager's eyes.&lt;/p&gt;

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

&lt;p&gt;The real value of AI in this story isn't that it wrote my CV. It's that it did the dirty work nobody does: trace every piece of advice back to its primary source and find that half of them are ghost citations. The CV-advice industry largely runs on folklore repeated with confidence, because nobody clicks the link.&lt;/p&gt;

&lt;p&gt;And the lesson that outlives the CV: I'd spent time optimizing my CV against a rejecting robot — a robot that doesn't exist. Meanwhile the real reader, the busy human who decides in a few seconds, was there the whole time. That's often the scam of "best practices": they make you please the wrong judge.&lt;/p&gt;

</description>
      <category>cv</category>
      <category>ats</category>
      <category>recruiting</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>The Ideal Cart: 5 Design Patterns That Earn Their Keep (and 2 Refused)</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 19 Jul 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/the-ideal-cart-5-design-patterns-that-earn-their-keep-and-2-refused-2nli</link>
      <guid>https://dev.to/ohugonnot/the-ideal-cart-5-design-patterns-that-earn-their-keep-and-2-refused-2nli</guid>
      <description>&lt;p&gt;It all starts with a review question. I was reworking my &lt;a href="https://www.web-developpeur.com/en/library/design-patterns/" rel="noopener noreferrer"&gt;notes on the Design Patterns book&lt;/a&gt;, the Gang of Four one, with examples that all came from the same universe: an online shop. Shipping fees for Strategy, the order for State, stock for Observer. And the question landed: "so what would the ideal cart be? One class that uses all the patterns?"&lt;/p&gt;

&lt;p&gt;Excellent question. Wrong direction. And the answer deserves more than a paragraph, because it contains roughly everything I know about design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: the ultimate Cart class
&lt;/h2&gt;

&lt;p&gt;The starting intuition is healthy: if all five examples come from the same shop, why not assemble everything? The trap is the word "class". A Cart class stacking Strategy, Decorator, State, Observer and a Facade "to show off" is exactly what the 1994 book calls pattern fever, and it warns against it as early as page 31: a pattern should only be applied when the flexibility it brings is actually needed.&lt;/p&gt;

&lt;p&gt;The ideal cart is not a class. It is a small architecture where each pattern holds the exact post where it earns its keep. And the hiring criterion fits in one question, the most useful one in the book: &lt;strong&gt;what is going to change?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Five hires, five nameable problems
&lt;/h2&gt;

&lt;p&gt;So I built the full checkout flow, starting from zero and introducing each pattern only when a real request demanded it. Here is the hiring log:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shipping fees vary → Strategy.&lt;/strong&gt; Standard post, express, store pickup: three calculations for the same question, "how much?". One &lt;code&gt;ShippingFee&lt;/code&gt; interface, one class per carrier. Adding a new carrier tomorrow: one class, zero modification elsewhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ShippingFee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Cart&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StandardPost&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShippingFee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Cart&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;4.99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The promo stacks → Decorator.&lt;/strong&gt; "Free shipping over €50" is not a property of the carriers, it is a wrapper around them. Same interface as what it wraps: to the rest of the code, a decorated carrier is a carrier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FreeShippingOver&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShippingFee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;ShippingFee&lt;/span&gt; &lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Cart&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The order has life stages → State.&lt;/strong&gt; "Cancel" does not mean the same thing in the cart (empty it), paid (refund) or shipped (carrier return). Instead of an if/elseif on the status duplicated in every method, the order carries a state object it swaps at each stage, and &lt;code&gt;cancel()&lt;/code&gt; delegates. Zero ifs, forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paying must trigger the unknown → Observer.&lt;/strong&gt; Email, stock, accounting, and the SMS marketing will ask for next month. The &lt;code&gt;pay()&lt;/code&gt; method must not know that list: it announces, and subscribers react. A list of callbacks and one loop: that is the whole pattern, and it is the same one as your &lt;code&gt;addEventListener&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The controller wants one button → Facade.&lt;/strong&gt; A &lt;code&gt;Checkout&lt;/code&gt; object receives the shipping strategy (possibly decorated) and the event bus, and exposes one &lt;code&gt;placeOrder()&lt;/code&gt; method. The code receiving the POST knows only it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that changes everything: where decisions live
&lt;/h2&gt;

&lt;p&gt;The most important piece of the ideal cart is none of the five patterns. It is the assembly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The ONLY place in the program that knows the concrete classes.&lt;/span&gt;
&lt;span class="nv"&gt;$checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FreeShippingOver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StandardPost&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// a Decorator around a Strategy&lt;/span&gt;
    &lt;span class="nv"&gt;$events&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;Every concrete choice is made at the root, at assembly time. &lt;code&gt;Checkout&lt;/code&gt; receives interfaces: it does not know whether shipping is decorated, nor who subscribed to the events. If you have read my &lt;a href="https://www.web-developpeur.com/en/library/clean-architecture/" rel="noopener noreferrer"&gt;Clean Architecture notes&lt;/a&gt;, you recognize the dependency rule: the two books, written twenty-three years apart, converge on exactly the same move.&lt;/p&gt;

&lt;p&gt;And the second lesson is my favorite: &lt;strong&gt;the most central class is the dumbest&lt;/strong&gt;. The Cart itself uses no pattern. It adds up lines. The patterns live around it, at the places that change, never at the center.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two refusals (as important as the hires)
&lt;/h2&gt;

&lt;p&gt;Along the way, two patterns applied and were turned down. A &lt;strong&gt;Singleton&lt;/strong&gt; (&lt;code&gt;Cart::getInstance()&lt;/code&gt;, "to access the cart from anywhere"): a global variable in disguise, untestable, and root injection already covers the need. A &lt;strong&gt;Command&lt;/strong&gt; with an undo/redo history, "just in case": nobody asked to cancel step by step, and a pattern installed for an imaginary need is complexity paid upfront.&lt;/p&gt;

&lt;p&gt;A good design shows in its absent patterns as much as its present ones. The list of what your code refuses to do says more than the list of what it can do; it is the same idea as &lt;a href="https://www.web-developpeur.com/en/blog/progres-langages-soustraction" rel="noopener noreferrer"&gt;language progress by subtraction&lt;/a&gt;, one floor down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result, playable
&lt;/h2&gt;

&lt;p&gt;None of this stayed a drawing. The shop actually runs, and I turned it into the 13th guided project of the learning section: &lt;a href="https://www.web-developpeur.com/apprendre/projets/panier-design-patterns/" rel="noopener noreferrer"&gt;"The pattern shop"&lt;/a&gt; (in French), where the checkout gets built step by step, with the real AI prompts, the first drafts that crack, two predictions to make before reading, and a final challenge (a second promo, stacked on the first, without modifying a single existing class).&lt;/p&gt;

&lt;p&gt;Every button in the demo goes through a pattern, and an event log at the bottom shows the Observer subscribers waking up when you pay. If you just want the skeleton, it is also in the &lt;a href="https://www.web-developpeur.com/en/library/design-patterns/" rel="noopener noreferrer"&gt;book notes&lt;/a&gt;, as a boxed aside.&lt;/p&gt;

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

&lt;p&gt;What stays with me from this exercise is not the code, it is the order of operations. Not once did I ask "which pattern should I put here?". I listed the shop's likely requests, and each pattern arrived as the answer to one precise request, with a problem name attached. The two times a pattern showed up without a problem to solve, it was sent home.&lt;/p&gt;

&lt;p&gt;Maybe that is the real definition of the ideal cart: not the one containing the most patterns, but the one where every pattern can answer the question "what are you here for?" without stammering.&lt;/p&gt;

</description>
      <category>designpatterns</category>
      <category>php</category>
      <category>oop</category>
      <category>strategy</category>
    </item>
    <item>
      <title>Big O for the Impatient: Why Your Loop Is Slow (and When It Matters)</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sat, 18 Jul 2026 09:00:04 +0000</pubDate>
      <link>https://dev.to/ohugonnot/big-o-for-the-impatient-why-your-loop-is-slow-and-when-it-matters-30le</link>
      <guid>https://dev.to/ohugonnot/big-o-for-the-impatient-why-your-loop-is-slow-and-when-it-matters-30le</guid>
      <description>&lt;p&gt;The page loads in 80 ms locally. In prod, on the real database, it takes 9 seconds and freezes the tab. The code did not change. The data did: 60 test rows locally, 14,000 in production.&lt;/p&gt;

&lt;p&gt;The culprit was a ten-line loop, perfectly readable, hunting duplicates in a list. It had no bug. It just had the wrong &lt;em&gt;shape&lt;/em&gt;. And understanding that shape is all Big O is about. No math, promise: just enough to read your own code and know which part will explode as the data grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Big O measures a shape, not a speed
&lt;/h2&gt;

&lt;p&gt;First intuition to break: &lt;code&gt;O(...)&lt;/code&gt; does not tell you "this takes 3 milliseconds". It tells you &lt;strong&gt;how the time reacts when you double the data&lt;/strong&gt;. It is a curve shape, not a stopwatch. Three families cover the overwhelming majority of the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;O(1)&lt;/code&gt; — constant.&lt;/strong&gt; Data size changes nothing. Reading &lt;code&gt;array[5000]&lt;/code&gt; is as fast as &lt;code&gt;array[5]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;O(n)&lt;/code&gt; — linear.&lt;/strong&gt; Twice the data, twice the time. A loop that walks everything.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;O(n²)&lt;/code&gt; — quadratic.&lt;/strong&gt; Twice the data, &lt;em&gt;four&lt;/em&gt; times the time. The killer class, and the one my loop had.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three growth curves: O(1) flat, O(n) straight, O(n²) exploding amount of data (n) → time → O(1) O(n) O(n²) at small scale, all three look alike then O(n²) breaks away&lt;/p&gt;

&lt;p&gt;The trap: on 60 test rows, all three curves are glued together. It is at production scale that O(n²) breaks away.&lt;/p&gt;

&lt;p&gt;That is why my bug was invisible locally: at n = 60, &lt;code&gt;O(n)&lt;/code&gt; does 60 operations and &lt;code&gt;O(n²)&lt;/code&gt; does 3,600. The difference is microseconds, nobody sees it. At n = 14,000, &lt;code&gt;O(n)&lt;/code&gt; does 14,000 operations, &lt;code&gt;O(n²)&lt;/code&gt; does &lt;strong&gt;196 million&lt;/strong&gt;. That, you feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop that breaks away
&lt;/h2&gt;

&lt;p&gt;The offending code, stripped to the bone: for each element, I rescan the whole list to see if it appears twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✗ O(n²): a loop INSIDE a loop&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dupes&lt;/span&gt; &lt;span class="o"&gt;=&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;        &lt;span class="c1"&gt;// ← the rescan that costs&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;dupes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same trap hides in a sneakier shape, the one that got me for years: &lt;code&gt;includes()&lt;/code&gt; inside a loop. It looks like a single loop, but &lt;code&gt;includes&lt;/code&gt; hides a second one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✗ disguised O(n²): each includes() rescans the whole array&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// includes = O(n), in a loop = O(n²)&lt;/span&gt;
    &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is one word: &lt;code&gt;Set&lt;/code&gt;. A &lt;code&gt;Set&lt;/code&gt; (or an object, or a &lt;code&gt;Map&lt;/code&gt;) is a &lt;strong&gt;hash table&lt;/strong&gt;: checking membership with &lt;code&gt;.has()&lt;/code&gt; is &lt;code&gt;O(1)&lt;/code&gt;, whatever the size. The double loop collapses into a single one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✓ O(n): the Set answers in O(1), one pass is enough&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dupes&lt;/span&gt; &lt;span class="o"&gt;=&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;dupes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// O(1)&lt;/span&gt;
    &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;My 9 seconds dropped to 40 milliseconds. No clever optimization, no cache: just the right curve &lt;em&gt;shape&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Set is magic: array versus linked list
&lt;/h2&gt;

&lt;p&gt;To see where that &lt;code&gt;O(1)&lt;/code&gt; comes from, you have to open the hood of data structures. It all rests on one question: &lt;strong&gt;where do the elements live in memory?&lt;/strong&gt; Memory is one long row of numbered cells.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;array&lt;/strong&gt; stores its elements in cells glued next to each other. Since it knows the start address, it instantly computes where the 5000th lives: read in &lt;code&gt;O(1)&lt;/code&gt;. The flip side: inserting at the front forces everything else to shift over by one, so &lt;code&gt;O(n)&lt;/code&gt;. That is exactly what &lt;code&gt;unshift()&lt;/code&gt; does in JavaScript, where &lt;code&gt;push()&lt;/code&gt; (append at the end) is free.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;linked list&lt;/strong&gt; does the opposite: its elements are scattered anywhere, and each holds the address of the next, like a treasure hunt. Inserting shifts nothing (&lt;code&gt;O(1)&lt;/code&gt;), but reading the 5000th forces you to follow the arrows one by one from the start (&lt;code&gt;O(n)&lt;/code&gt;). You will almost never write one by hand: in PHP it is &lt;code&gt;SplDoublyLinkedList&lt;/code&gt;, in Python &lt;code&gt;collections.deque&lt;/code&gt;, in Go &lt;code&gt;container/list&lt;/code&gt;, and in JavaScript… you code it yourself.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;hash table&lt;/strong&gt; (the &lt;code&gt;Set&lt;/code&gt;, the &lt;code&gt;Map&lt;/code&gt;, Python's &lt;code&gt;dict&lt;/code&gt;, PHP's &lt;code&gt;array&lt;/code&gt;) plays in another league: a function turns the key directly into a memory position. No walking, no shifting: read, write, test membership, all in &lt;code&gt;O(1)&lt;/code&gt;. That is why swapping an &lt;code&gt;array.includes()&lt;/code&gt; for a &lt;code&gt;Set.has()&lt;/code&gt; turns a quadratic curve into a linear one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden O(n) behind an innocent line
&lt;/h2&gt;

&lt;p&gt;The real skill is not reciting these classes, it is &lt;strong&gt;spotting them in your own code&lt;/strong&gt;, often tucked behind innocent syntax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;array.unshift(x)&lt;/code&gt; and a mid-array &lt;code&gt;array.splice(i, 0, x)&lt;/code&gt;: &lt;code&gt;O(n)&lt;/code&gt;, everything shifts.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;array.includes(x)&lt;/code&gt; or &lt;code&gt;array.indexOf(x)&lt;/code&gt; inside a loop: &lt;code&gt;O(n²)&lt;/code&gt;. A &lt;code&gt;Set&lt;/code&gt; or a &lt;code&gt;Map&lt;/code&gt; brings it back to &lt;code&gt;O(n)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  An SQL query &lt;code&gt;WHERE email = ?&lt;/code&gt; with no index on the column: the database does a &lt;em&gt;full scan&lt;/em&gt;, &lt;code&gt;O(n)&lt;/code&gt; over millions of rows. The index is its version of binary search, &lt;code&gt;O(log n)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  A &lt;code&gt;.find()&lt;/code&gt; inside a &lt;code&gt;.map()&lt;/code&gt; to "join" two lists: again a loop in a loop. Index one of the two lists into a &lt;code&gt;Map&lt;/code&gt; first.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Nobody will ask you to rewrite a hash table, your language ships one. That is not what Big O is for. It is for reading a loop and &lt;em&gt;seeing its future&lt;/em&gt;: this one will hold at a million elements, that one will freeze the page. It is a reading skill, not a writing one.&lt;/p&gt;

&lt;p&gt;And the irony is that AI changes nothing here, quite the opposite. An assistant happily produces an &lt;code&gt;O(n²)&lt;/code&gt; loop that passes every test on 50 rows and dies in prod. Recognizing the curve shape in generated code is something you cannot delegate. If the topic clicks, I wrote a whole &lt;a href="https://www.web-developpeur.com/en/library/grokking-algorithms/" rel="noopener noreferrer"&gt;reading note on &lt;em&gt;Grokking Algorithms&lt;/em&gt;&lt;/a&gt;, the book that finally reconciled me with algorithms without a single formal proof.&lt;/p&gt;

</description>
      <category>bigo</category>
      <category>algorithms</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
