<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ifekri.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ifekri.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-08-14T20:40:21-04:00</updated><id>https://ifekri.github.io/feed.xml</id><title type="html">iFekri</title><subtitle>Developer portfolio and field notes. Built for people who ship.</subtitle><entry><title type="html">Content hashing for expensive stages</title><link href="https://ifekri.github.io/posts/content-hashing-for-expensive-stages/" rel="alternate" type="text/html" title="Content hashing for expensive stages" /><published>2026-08-14T03:00:00-04:00</published><updated>2026-08-14T03:00:00-04:00</updated><id>https://ifekri.github.io/posts/content-hashing-for-expensive-stages</id><content type="html" xml:base="https://ifekri.github.io/posts/content-hashing-for-expensive-stages/"><![CDATA[<p>A late-stage failure in my pipeline meant rerunning from the top, which meant paying for image generation again. The images had not changed. Nothing about their inputs had changed. I was buying identical output twice because the pipeline had no way to know it was identical.</p>

<!--more-->

<h2 id="key-on-inputs-not-on-filenames">Key on inputs, not on filenames</h2>

<p>The usual instinct is to check whether the output file exists and skip if so. That fails in the direction that hurts: a stale file with the right name silently satisfies the check and you ship the wrong content.</p>

<p>Hash what went in instead.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">hashlib</span><span class="p">,</span> <span class="n">json</span>

<span class="k">def</span> <span class="nf">stage_key</span><span class="p">(</span><span class="n">stage_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">inputs</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="n">payload</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">(</span><span class="n">inputs</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="n">digest</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">payload</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">hexdigest</span><span class="p">()[:</span><span class="mi">16</span><span class="p">]</span>
    <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">stage_name</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">digest</span><span class="si">}</span><span class="sh">"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sort_keys=True</code> matters. Without it, two identical dicts serialise differently depending on insertion order and you get cache misses that look like cache bugs.</p>

<h2 id="include-everything-that-changes-the-output">Include everything that changes the output</h2>

<p>This is where it goes wrong. Miss a field and you serve stale results after a change that should have invalidated them.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">key</span> <span class="o">=</span> <span class="nf">stage_key</span><span class="p">(</span><span class="sh">"</span><span class="s">image</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">prompt</span><span class="sh">"</span><span class="p">:</span> <span class="n">scene</span><span class="p">.</span><span class="n">prompt</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="n">model_id</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">style_preset</span><span class="sh">"</span><span class="p">:</span> <span class="n">config</span><span class="p">.</span><span class="n">style</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">aspect_ratio</span><span class="sh">"</span><span class="p">:</span> <span class="n">config</span><span class="p">.</span><span class="n">aspect_ratio</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">seed</span><span class="sh">"</span><span class="p">:</span> <span class="n">scene</span><span class="p">.</span><span class="n">seed</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">prompt_template_version</span><span class="sh">"</span><span class="p">:</span> <span class="n">TEMPLATE_VERSION</span><span class="p">,</span>   <span class="c1"># bump when you edit it
</span><span class="p">})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">prompt_template_version</code> is the one people forget. Edit the base template, and every prompt changes even though no scene did. Without a version in the key, the whole library goes stale invisibly.</p>

<h2 id="store-the-result-next-to-the-key">Store the result next to the key</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">cached</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">produce</span><span class="p">,</span> <span class="n">store</span><span class="p">:</span> <span class="n">Path</span><span class="p">):</span>
    <span class="n">marker</span> <span class="o">=</span> <span class="n">store</span> <span class="o">/</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">key</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="sh">'</span><span class="si">:</span><span class="sh">'</span><span class="s">, </span><span class="sh">'</span><span class="n">_</span><span class="sh">'</span><span class="s">)</span><span class="si">}</span><span class="s">.json</span><span class="sh">"</span>

    <span class="k">if</span> <span class="n">marker</span><span class="p">.</span><span class="nf">exists</span><span class="p">():</span>
        <span class="n">meta</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">marker</span><span class="p">.</span><span class="nf">read_text</span><span class="p">())</span>
        <span class="k">if</span> <span class="nc">Path</span><span class="p">(</span><span class="n">meta</span><span class="p">[</span><span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="p">]).</span><span class="nf">exists</span><span class="p">():</span>
            <span class="k">return</span> <span class="n">meta</span><span class="p">[</span><span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="p">]</span>
        <span class="n">marker</span><span class="p">.</span><span class="nf">unlink</span><span class="p">()</span>          <span class="c1"># artifact gone, drop the marker
</span>
    <span class="n">path</span> <span class="o">=</span> <span class="nf">produce</span><span class="p">()</span>
    <span class="n">marker</span><span class="p">.</span><span class="nf">write_text</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span>
        <span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="p">:</span> <span class="nf">str</span><span class="p">(</span><span class="n">path</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">created</span><span class="sh">"</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">).</span><span class="nf">isoformat</span><span class="p">(),</span>
    <span class="p">}))</span>
    <span class="k">return</span> <span class="n">path</span>
</code></pre></div></div>

<p>Checking that the artifact still exists is what keeps this honest. A marker pointing at a deleted file is worse than no marker.</p>

<h2 id="retries-become-free">Retries become free</h2>

<p>The second benefit is bigger than the caching. A stage keyed on its input is idempotent, so a retry after a transient failure re-does only the work that did not complete.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">attempt</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">max_attempts</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">cached</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="nf">call_api</span><span class="p">(</span><span class="n">scene</span><span class="p">),</span> <span class="n">store</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">TransientError</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">attempt</span> <span class="o">==</span> <span class="n">max_attempts</span> <span class="o">-</span> <span class="mi">1</span><span class="p">:</span>
            <span class="k">raise</span>
        <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="n">base_delay</span> <span class="o">*</span> <span class="p">(</span><span class="mi">2</span> <span class="o">**</span> <span class="n">attempt</span><span class="p">)</span> <span class="o">+</span> <span class="n">random</span><span class="p">.</span><span class="nf">uniform</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">))</span>
</code></pre></div></div>

<p>Without idempotency, a retry policy on a paid API is a policy for spending more money.</p>

<h2 id="clean-test-residue">Clean test residue</h2>

<p>One that bit me: a failed-items database left over from testing contained fabricated entries. On the next real run they fed straight into the retry path and produced work nobody asked for.</p>

<p>Keep scratch output out of the directories the pipeline reads. If a test writes into the live tree, it is not a test any more — it is an input.</p>

<h2 id="what-it-is-worth">What it is worth</h2>

<p>At my measured rate, image generation is 96% of per-video cost. A single avoided rerun pays for the afternoon it took to build this. Across a library where many scenes are semantically near-identical, the cache stops being a retry optimisation and becomes a margin.</p>]]></content><author><name></name></author><category term="Pipelines" /><category term="caching" /><category term="idempotency" /><category term="architecture" /><category term="python" /><summary type="html"><![CDATA[When a stage costs real money, rerunning it after a downstream failure is a bill you should not be paying twice.]]></summary></entry><entry><title type="html">Not every decision needs a model</title><link href="https://ifekri.github.io/posts/not-every-decision-needs-a-model/" rel="alternate" type="text/html" title="Not every decision needs a model" /><published>2026-08-12T09:50:00-04:00</published><updated>2026-08-12T09:50:00-04:00</updated><id>https://ifekri.github.io/posts/not-every-decision-needs-a-model</id><content type="html" xml:base="https://ifekri.github.io/posts/not-every-decision-needs-a-model/"><![CDATA[<p>My pipeline called a language model to plan every scene in a video — shot type, camera angle, framing. It worked. It was also the wrong tool, and I only saw it after writing the deterministic version.</p>

<!--more-->

<h2 id="what-the-model-was-actually-deciding">What the model was actually deciding</h2>

<p>“Should this scene be a close-up or a wide shot?”</p>

<p>That is not a language problem. It is a weighted choice constrained by a few rules: vary the framing, avoid jarring transitions, tighten during emotional peaks, open up at the start and end.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SHOT_WEIGHTS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">extreme_closeup</span><span class="sh">"</span><span class="p">:</span> <span class="mf">0.10</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">closeup</span><span class="sh">"</span><span class="p">:</span>         <span class="mf">0.25</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">medium</span><span class="sh">"</span><span class="p">:</span>          <span class="mf">0.30</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">wide</span><span class="sh">"</span><span class="p">:</span>            <span class="mf">0.25</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">overhead</span><span class="sh">"</span><span class="p">:</span>        <span class="mf">0.05</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">insert</span><span class="sh">"</span><span class="p">:</span>          <span class="mf">0.05</span><span class="p">,</span>
<span class="p">}</span>

<span class="n">INVALID_TRANSITIONS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">extreme_closeup</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">wide</span><span class="sh">"</span><span class="p">),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">wide</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">extreme_closeup</span><span class="sh">"</span><span class="p">),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">overhead</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">extreme_closeup</span><span class="sh">"</span><span class="p">),</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A hundred lines of Python covers it, and covers it better.</p>

<h2 id="constraints-are-easier-to-enforce-than-to-describe">Constraints are easier to enforce than to describe</h2>

<p>Telling a model “do not use the same shot more than twice in a row” is a request. In code it is a guarantee.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">next_shot</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">total</span><span class="p">,</span> <span class="n">intensity</span><span class="o">=</span><span class="mf">0.5</span><span class="p">):</span>
    <span class="n">weights</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_adjust_for_arc</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="n">total</span><span class="p">,</span> <span class="n">intensity</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">recent</span><span class="p">:</span>
        <span class="n">last</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">recent</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="nf">_run_length</span><span class="p">(</span><span class="n">last</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">self</span><span class="p">.</span><span class="n">max_consecutive</span><span class="p">:</span>
            <span class="n">weights</span><span class="p">[</span><span class="n">last</span><span class="p">]</span> <span class="o">=</span> <span class="mf">0.0</span>          <span class="c1"># hard block, not a nudge
</span>
    <span class="n">shot</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_weighted_choice</span><span class="p">(</span><span class="n">weights</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">recent</span> <span class="ow">and</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">recent</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">shot</span><span class="p">)</span> <span class="ow">in</span> <span class="n">INVALID_TRANSITIONS</span><span class="p">:</span>
        <span class="n">shot</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_pick_valid</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">recent</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">weights</span><span class="p">)</span>

    <span class="n">self</span><span class="p">.</span><span class="n">recent</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">shot</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">shot</span>
</code></pre></div></div>

<p>One detail there cost me real output quality. My first version damped the repeated shot’s weight to <code class="language-plaintext highlighter-rouge">0.1</code> instead of zeroing it. Runs of three still slipped through, and a limit that is <em>usually</em> enforced is not a limit. Set it to zero.</p>

<h2 id="seeding-buys-you-reproducibility">Seeding buys you reproducibility</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">planner</span> <span class="o">=</span> <span class="nc">ShotPlanner</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="sh">"</span><span class="s">mixed</span><span class="sh">"</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
</code></pre></div></div>

<p>Same seed, same plan, every time. That means you can regenerate a job after fixing something downstream and get an identical structure, and you can write tests against the planner:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_no_long_runs</span><span class="p">():</span>
    <span class="n">p</span> <span class="o">=</span> <span class="nc">ShotPlanner</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="sh">"</span><span class="s">mixed</span><span class="sh">"</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">seq</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">.</span><span class="nf">next_shot</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="mi">149</span><span class="p">).</span><span class="n">value</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">149</span><span class="p">)]</span>

    <span class="n">run</span> <span class="o">=</span> <span class="n">worst</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">seq</span><span class="p">,</span> <span class="n">seq</span><span class="p">[</span><span class="mi">1</span><span class="p">:]):</span>
        <span class="n">run</span> <span class="o">=</span> <span class="n">run</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span> <span class="k">else</span> <span class="mi">1</span>
        <span class="n">worst</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">worst</span><span class="p">,</span> <span class="n">run</span><span class="p">)</span>

    <span class="k">assert</span> <span class="n">worst</span> <span class="o">&lt;=</span> <span class="mi">2</span><span class="p">,</span> <span class="sa">f</span><span class="sh">"</span><span class="s">run of </span><span class="si">{</span><span class="n">worst</span><span class="si">}</span><span class="sh">"</span>
</code></pre></div></div>

<p>You cannot write that test against a model call.</p>

<h2 id="the-comparison">The comparison</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>model call</th>
      <th>weighted table</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>latency</td>
      <td>seconds</td>
      <td>microseconds</td>
    </tr>
    <tr>
      <td>cost per job</td>
      <td>non-zero</td>
      <td>zero</td>
    </tr>
    <tr>
      <td>reproducible</td>
      <td>no</td>
      <td>with a seed</td>
    </tr>
    <tr>
      <td>testable</td>
      <td>not really</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>enforces hard limits</td>
      <td>asks</td>
      <td>guarantees</td>
    </tr>
  </tbody>
</table>

<h2 id="where-the-model-still-earns-its-place">Where the model still earns its place</h2>

<p>Interpreting the actual content of a scene. “This passage is about betrayal in a rain-soaked alley” is a language task, and no lookup table produces it.</p>

<p>Split the two. Let the model read meaning; let code decide structure. In my pipeline that removed an entire class of API calls, made output reproducible, and — the part I did not expect — improved consistency, because rules that are enforced beat rules that are requested.</p>

<p>One warning from my own experience: the deterministic planner sat in my codebase for months, disabled by an argument-name mismatch, silently falling back to the model on every run. Log your fallbacks loudly. A silent fallback is a feature you are paying for and not receiving.</p>]]></content><author><name></name></author><category term="Architecture" /><category term="llm" /><category term="determinism" /><category term="architecture" /><category term="cost-optimisation" /><summary type="html"><![CDATA[I was paying a language model to make choices that a weighted table makes better: faster, cheaper, reproducible, and testable.]]></summary></entry><entry><title type="html">Your price table is wrong</title><link href="https://ifekri.github.io/posts/your-price-table-is-wrong/" rel="alternate" type="text/html" title="Your price table is wrong" /><published>2026-08-08T04:05:00-04:00</published><updated>2026-08-08T04:05:00-04:00</updated><id>https://ifekri.github.io/posts/your-price-table-is-wrong</id><content type="html" xml:base="https://ifekri.github.io/posts/your-price-table-is-wrong/"><![CDATA[<p>I built cost tracking, wired a rate table into it, and got a number I trusted. Then I made one real call and compared it to what I had been reporting. The estimate was 13% low.</p>

<!--more-->

<h2 id="estimates-drift-for-reasons-you-cannot-see">Estimates drift for reasons you cannot see</h2>

<p>A hardcoded rate table goes stale in every direction at once. Providers change pricing. Your model string silently falls back to a more expensive one when the primary is unavailable. Token counting differs from the provider’s own accounting. Requests carry overhead you never counted.</p>

<p>None of these announce themselves. Your total stays plausible while being consistently wrong.</p>

<h2 id="ask-the-provider-instead">Ask the provider instead</h2>

<p>Most gateways will return the billed amount if you ask. On an OpenAI-compatible endpoint it is one field:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">:</span> <span class="n">messages</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">usage</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">include</span><span class="sh">"</span><span class="p">:</span> <span class="bp">True</span><span class="p">},</span>      <span class="c1"># &lt;- this line
</span><span class="p">}</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="n">endpoint</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="n">payload</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">120</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">resp</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span>

<span class="n">usage</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">usage</span><span class="sh">"</span><span class="p">,</span> <span class="p">{})</span>
<span class="k">if</span> <span class="sh">"</span><span class="s">cost</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">usage</span><span class="p">:</span>
    <span class="n">cost</span><span class="p">,</span> <span class="n">estimated</span> <span class="o">=</span> <span class="nf">float</span><span class="p">(</span><span class="n">usage</span><span class="p">[</span><span class="sh">"</span><span class="s">cost</span><span class="sh">"</span><span class="p">]),</span> <span class="bp">False</span>
<span class="k">else</span><span class="p">:</span>
    <span class="n">cost</span><span class="p">,</span> <span class="n">estimated</span> <span class="o">=</span> <span class="nf">estimate_from_table</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">usage</span><span class="p">),</span> <span class="bp">True</span>
</code></pre></div></div>

<p>Fall back to the table rather than replacing it. Not every provider returns cost, and you still want a number for the ones that do not.</p>

<h2 id="mark-every-row-with-its-provenance">Mark every row with its provenance</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>provider    model (purpose)                       calls      cost
---------------------------------------------------------------
openrouter  gemini-flash-image (image)              149    5.0735
elevenlabs  scribe (stt)                              6    0.1492  EST
openrouter  gemini-flash-lite (director)              1    0.0181
---------------------------------------------------------------
TOTAL                                                     5.2408
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">EST</code> flag is not decoration. It tells you which rows you may price against and which you may not. When somebody asks what a unit costs, you can answer precisely for the measured portion and honestly about the rest.</p>

<h2 id="verify-at-volume-not-once">Verify at volume, not once</h2>

<p>One call proves the field parses. It does not prove the provider returns it reliably.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">events</span> <span class="o">=</span> <span class="p">[</span><span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">l</span><span class="p">)</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="nf">open</span><span class="p">(</span><span class="n">log_path</span><span class="p">)]</span>
<span class="n">actual</span> <span class="o">=</span> <span class="nf">sum</span><span class="p">(</span><span class="mi">1</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">events</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">e</span><span class="p">[</span><span class="sh">"</span><span class="s">estimated</span><span class="sh">"</span><span class="p">])</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">actual</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">events</span><span class="p">)</span><span class="si">}</span><span class="s"> actual</span><span class="sh">"</span><span class="p">)</span>

<span class="n">costs</span> <span class="o">=</span> <span class="p">[</span><span class="n">e</span><span class="p">[</span><span class="sh">"</span><span class="s">cost_usd</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">events</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">e</span><span class="p">[</span><span class="sh">"</span><span class="s">estimated</span><span class="sh">"</span><span class="p">]]</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">min </span><span class="si">{</span><span class="nf">min</span><span class="p">(</span><span class="n">costs</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="si">}</span><span class="s">  mean </span><span class="si">{</span><span class="nf">sum</span><span class="p">(</span><span class="n">costs</span><span class="p">)</span><span class="o">/</span><span class="nf">len</span><span class="p">(</span><span class="n">costs</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="si">}</span><span class="s">  max </span><span class="si">{</span><span class="nf">max</span><span class="p">(</span><span class="n">costs</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Twenty of twenty came back actual, clustered between $0.03377 and $0.03452. Tight clustering is itself a signal — wide spread would have meant a fallback model was being used more often than I thought.</p>

<h2 id="update-the-fallback-with-what-you-learned">Update the fallback with what you learned</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PRICES</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">gemini-flash-image</span><span class="sh">"</span><span class="p">:</span> <span class="mf">0.034</span><span class="p">,</span>   <span class="c1"># measured 2026-08, was 0.030 (est.)
</span><span class="p">}</span>
</code></pre></div></div>

<p>Even with real cost available, keep the table honest. It is what runs when the provider omits the field, and a stale fallback reintroduces the same drift through a smaller door.</p>

<h2 id="the-number-that-matters">The number that matters</h2>

<p>At the measured rate, a 149-image job is $5.07 rather than the $4.47 I had been reporting. On one video that is 60 cents. On a price list built for thousands of them, it is the difference between a margin and a slow loss.</p>

<p>Measure the thing you sell. Estimate only what you cannot measure, and label it.</p>]]></content><author><name></name></author><category term="Engineering" /><category term="api" /><category term="cost-optimisation" /><category term="telemetry" /><category term="openrouter" /><summary type="html"><![CDATA[I estimated $0.030 per image. The provider billed $0.034. Thirteen percent is invisible in a monthly total and fatal in a margin.]]></summary></entry><entry><title type="html">UFW does not protect Docker ports</title><link href="https://ifekri.github.io/posts/ufw-does-not-protect-docker-ports/" rel="alternate" type="text/html" title="UFW does not protect Docker ports" /><published>2026-08-03T07:40:00-04:00</published><updated>2026-08-03T07:40:00-04:00</updated><id>https://ifekri.github.io/posts/ufw-does-not-protect-docker-ports</id><content type="html" xml:base="https://ifekri.github.io/posts/ufw-does-not-protect-docker-ports/"><![CDATA[<p>I locked a server down, checked <code class="language-plaintext highlighter-rouge">ufw status</code>, and saw no rule for port 8000. Then I opened <code class="language-plaintext highlighter-rouge">http://server-ip:8000</code> from another network and my admin dashboard loaded.</p>

<!--more-->

<h2 id="where-the-packet-actually-goes">Where the packet actually goes</h2>

<p>UFW writes rules into the <code class="language-plaintext highlighter-rouge">INPUT</code> chain. Traffic to a published container port does not traverse <code class="language-plaintext highlighter-rouge">INPUT</code> — Docker DNATs it and it passes through <code class="language-plaintext highlighter-rouge">FORWARD</code>, where Docker inserts its own accept rules during startup.</p>

<p>So <code class="language-plaintext highlighter-rouge">ufw status</code> is accurate about what it manages. It just does not manage this.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># UFW's view — nothing on 8000</span>
<span class="nb">sudo </span>ufw status numbered

<span class="c"># Reality</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-L</span> DOCKER <span class="nt">-n</span> | <span class="nb">grep </span>8000
</code></pre></div></div>

<p>Every port you publish with <code class="language-plaintext highlighter-rouge">-p</code> or a compose <code class="language-plaintext highlighter-rouge">ports:</code> entry is reachable from anywhere the network can reach the host, regardless of what UFW says.</p>

<h2 id="two-ways-to-fix-it">Two ways to fix it</h2>

<p><strong>Bind to loopback.</strong> The cleanest option when the service is only for you.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">panel</span><span class="pi">:</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">127.0.0.1:8000:8000"</span>   <span class="c1"># not "8000:8000"</span>
</code></pre></div></div>

<p>Reach it over an SSH tunnel or a private mesh network. A port that is not listening on a public interface cannot be scanned.</p>

<p><strong>Filter in DOCKER-USER.</strong> Docker provides this chain specifically so your rules survive its own rule generation, and it is evaluated before Docker’s accepts.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>iptables <span class="nt">-I</span> DOCKER-USER <span class="nt">-p</span> tcp <span class="nt">--dport</span> 8000 <span class="se">\</span>
  <span class="o">!</span> <span class="nt">-s</span> 203.0.113.10 <span class="nt">-j</span> DROP

<span class="nb">sudo </span>iptables <span class="nt">-L</span> DOCKER-USER <span class="nt">-n</span> <span class="nt">--line-numbers</span>
</code></pre></div></div>

<p>Persist it, or you lose the rule on the next reboot:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> iptables-persistent
<span class="nb">sudo </span>netfilter-persistent save
</code></pre></div></div>

<h2 id="read-the-log-not-the-config">Read the log, not the config</h2>

<p>When something is unreachable and you cannot tell why, the block log names the exact packet.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo grep</span> <span class="s1">'BLOCK'</span> /var/log/ufw.log | <span class="nb">tail</span> <span class="nt">-5</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[UFW BLOCK] IN=br-82f64cb60043 SRC=10.0.1.5 DST=10.0.0.1 DPT=9631 SYN
</code></pre></div></div>

<p>That single line told me something a dozen assumptions had not: my container network was on <code class="language-plaintext highlighter-rouge">10.0.0.0/8</code>, not the <code class="language-plaintext highlighter-rouge">172.16.0.0/12</code> default I had written a rule for. The rule was correct and matched nothing.</p>

<h2 id="rules-per-interface-go-stale">Rules per interface go stale</h2>

<p>Do not write rules against <code class="language-plaintext highlighter-rouge">veth</code> names. They are regenerated every time a container restarts.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Fragile — gone after the next restart</span>
<span class="nb">sudo </span>ufw allow <span class="k">in </span>on veth617b319 to any port 9631

<span class="c"># Durable — survives container and network recreation</span>
<span class="nb">sudo </span>ufw allow from 10.0.0.0/8 to 10.0.0.1 port 9631 proto tcp
</code></pre></div></div>

<p>Bridge names like <code class="language-plaintext highlighter-rouge">br-82f64cb60043</code> are more stable but still tied to a network that can be recreated. Address-based rules outlive both.</p>

<h2 id="audit-before-you-assume">Audit before you assume</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ss <span class="nt">-tulpn</span> | <span class="nb">grep </span>LISTEN
</code></pre></div></div>

<p>Anything bound to <code class="language-plaintext highlighter-rouge">0.0.0.0</code> is public unless something outside UFW is stopping it. Run this on a server you believe is locked down — the results are usually educational.</p>]]></content><author><name></name></author><category term="Infrastructure" /><category term="docker" /><category term="ufw" /><category term="iptables" /><category term="security" /><category term="linux" /><summary type="html"><![CDATA[Your firewall says a port is closed. Your admin panel answers on it from the open internet. Both are telling the truth.]]></summary></entry><entry><title type="html">Terraform modules that stay readable</title><link href="https://ifekri.github.io/posts/terraform-modules-that-stay-readable/" rel="alternate" type="text/html" title="Terraform modules that stay readable" /><published>2026-07-27T02:15:00-04:00</published><updated>2026-07-27T02:15:00-04:00</updated><id>https://ifekri.github.io/posts/terraform-modules-that-stay-readable</id><content type="html" xml:base="https://ifekri.github.io/posts/terraform-modules-that-stay-readable/"><![CDATA[<p>Terraform code rots faster than application code. Not because the language is bad, but because it’s easy to write infrastructure that works today and is incomprehensible in six months. A few structural habits prevent most of that.</p>

<!--more-->

<h2 id="modules-should-do-one-thing">Modules should do one thing</h2>

<p>A module that creates a VPC, a database, a cache, and a monitoring stack is not a module. It’s a novel. Split it.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf
  database/
    main.tf
    variables.tf
    outputs.tf
  monitoring/
    main.tf
    variables.tf
    outputs.tf
</code></pre></div></div>

<p>Each module should be understandable in one read. If you need to scroll to understand what a module creates, it’s doing too much.</p>

<h2 id="variables-need-contracts">Variables need contracts</h2>

<p>Every variable should have a type, a description, and a default when a sensible one exists.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"instance_type"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"EC2 instance class for the app servers"</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="s2">"t3.medium"</span>

  <span class="nx">validation</span> <span class="p">{</span>
    <span class="nx">condition</span>     <span class="o">=</span> <span class="nx">can</span><span class="p">(</span><span class="nx">regex</span><span class="p">(</span><span class="s2">"^t3</span><span class="err">\\</span><span class="s2">."</span><span class="p">,</span> <span class="nx">var</span><span class="p">.</span><span class="nx">instance_type</span><span class="p">))</span>
    <span class="nx">error_message</span> <span class="o">=</span> <span class="s2">"Only t3 instance types are supported for this workload."</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"enable_monitoring"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">bool</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Whether to attach CloudWatch agent and alarms"</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The validation block is not optional. It’s the difference between a typo caught at plan time and a production outage at apply time.</p>

<h2 id="outputs-are-the-api">Outputs are the API</h2>

<p>A module’s outputs are its public interface. Export what consumers need, nothing more.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">output</span> <span class="s2">"vpc_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"The ID of the created VPC"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"private_subnet_ids"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"IDs of the private subnets, for use by app modules"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">private</span><span class="p">[*].</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Don’t export every attribute of every resource. That couples consumers to implementation details you might want to change later.</p>

<h2 id="state-is-sacred">State is sacred</h2>

<p>Remote state, state locking, and a clear separation between environments. Non-negotiable.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">backend</span> <span class="s2">"s3"</span> <span class="p">{</span>
    <span class="nx">bucket</span>         <span class="o">=</span> <span class="s2">"terraform-state-prod"</span>
    <span class="nx">key</span>            <span class="o">=</span> <span class="s2">"app/terraform.tfstate"</span>
    <span class="nx">region</span>         <span class="o">=</span> <span class="s2">"us-east-1"</span>
    <span class="nx">dynamodb_table</span> <span class="o">=</span> <span class="s2">"terraform-locks"</span>
    <span class="nx">encrypt</span>        <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>One state file per environment per component. Never share state between dev and prod. Never run <code class="language-plaintext highlighter-rouge">terraform apply</code> from a laptop against production state.</p>

<h2 id="the-test-that-matters">The test that matters</h2>

<p>The best Terraform test is <code class="language-plaintext highlighter-rouge">terraform plan</code> in CI on every pull request. If the plan output is clean, the change is probably safe. If it’s a wall of red, something is wrong.</p>

<p>Readable infrastructure is a team sport. Write it for the person who has to change it at 2am, because eventually that person is you.</p>]]></content><author><name></name></author><category term="Infrastructure" /><category term="terraform" /><category term="iac" /><category term="aws" /><summary type="html"><![CDATA[How to structure Terraform so the next person doesn't curse your name.]]></summary></entry><entry><title type="html">sys.path.insert will shadow your modules</title><link href="https://ifekri.github.io/posts/sys-path-insert-will-shadow-your-modules/" rel="alternate" type="text/html" title="sys.path.insert will shadow your modules" /><published>2026-07-27T02:00:00-04:00</published><updated>2026-07-27T02:00:00-04:00</updated><id>https://ifekri.github.io/posts/sys-path-insert-will-shadow-your-modules</id><content type="html" xml:base="https://ifekri.github.io/posts/sys-path-insert-will-shadow-your-modules/"><![CDATA[<p>I needed a shared telemetry module reachable from every stage of a pipeline whose stages live in separate directories. The quick fix:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sys</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">PROJECT_ROOT</span><span class="p">)</span>
<span class="kn">import</span> <span class="n">cost_tracker</span>
</code></pre></div></div>

<p>It worked. It also introduced an <code class="language-plaintext highlighter-rouge">ImportError</code> in a module I had not touched, which only appeared under one import order.</p>

<!--more-->

<h2 id="what-insert0-actually-does">What insert(0) actually does</h2>

<p><code class="language-plaintext highlighter-rouge">sys.path</code> is searched in order. Putting the project root at position zero means it is searched <em>before</em> the directory of the module doing the importing.</p>

<p>My layout had a <code class="language-plaintext highlighter-rouge">config_manager.py</code> at the root and another inside a stage directory. They were different files with different contents, and the stage relied on its local one.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>project/
  config_manager.py            &lt;- generic
  cost_tracker.py
  03_image-generator/
    config_manager.py          &lt;- stage-specific
    generator.py               &lt;- expects the local one
    runpod_provider.py
</code></pre></div></div>

<p>After <code class="language-plaintext highlighter-rouge">insert(0, PROJECT_ROOT)</code>, <code class="language-plaintext highlighter-rouge">from config_manager import config</code> inside the stage resolved to the root file. Same name, different module, missing attribute.</p>

<h2 id="why-it-only-failed-sometimes">Why it only failed sometimes</h2>

<p>Import order decided the outcome. Python caches modules in <code class="language-plaintext highlighter-rouge">sys.modules</code> by name, so whichever import ran first won and the second silently received it.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Order A — generator imports its config first, caches the local one
</span><span class="kn">import</span> <span class="n">generator</span>            <span class="c1"># config_manager -&gt; stage-local
</span><span class="kn">import</span> <span class="n">runpod_provider</span>      <span class="c1"># gets the cached stage-local one, fine
</span>
<span class="c1"># Order B — runpod_provider goes first, and the root wins
</span><span class="kn">import</span> <span class="n">runpod_provider</span>      <span class="c1"># config_manager -&gt; root
</span><span class="kn">import</span> <span class="n">generator</span>            <span class="c1"># gets the cached root one, ImportError
</span></code></pre></div></div>

<p>In practice order A happened to be the common path, so the bug existed for a while without firing. Those are the ones that surface in production.</p>

<h2 id="the-fix">The fix</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sys</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">PROJECT_ROOT</span><span class="p">)</span>   <span class="c1"># not insert(0, ...)
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">append</code> puts the root last. Stage-local modules keep priority; the shared helper is still reachable because nothing else defines that name.</p>

<h2 id="verify-resolution-do-not-assume-it">Verify resolution, do not assume it</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">config_manager</span>
<span class="nf">print</span><span class="p">(</span><span class="n">config_manager</span><span class="p">.</span><span class="n">__file__</span><span class="p">)</span>
<span class="c1"># .../03_image-generator/config_manager.py   &lt;- correct
</span></code></pre></div></div>

<p>Test both orders explicitly. It takes one extra line and it is the only thing that catches this class of bug:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># tests/test_import_order.py
</span><span class="kn">import</span> <span class="n">subprocess</span><span class="p">,</span> <span class="n">sys</span>

<span class="k">for</span> <span class="n">first</span><span class="p">,</span> <span class="n">second</span> <span class="ow">in</span> <span class="p">[(</span><span class="sh">"</span><span class="s">generator</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">runpod_provider</span><span class="sh">"</span><span class="p">),</span>
                      <span class="p">(</span><span class="sh">"</span><span class="s">runpod_provider</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">generator</span><span class="sh">"</span><span class="p">)]:</span>
    <span class="n">code</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">import </span><span class="si">{</span><span class="n">first</span><span class="si">}</span><span class="s">, </span><span class="si">{</span><span class="n">second</span><span class="si">}</span><span class="s">; import config_manager; print(config_manager.__file__)</span><span class="sh">"</span>
    <span class="n">out</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="n">sys</span><span class="p">.</span><span class="n">executable</span><span class="p">,</span> <span class="sh">"</span><span class="s">-c</span><span class="sh">"</span><span class="p">,</span> <span class="n">code</span><span class="p">],</span> <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">assert</span> <span class="sh">"</span><span class="s">03_image-generator</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">out</span><span class="p">.</span><span class="n">stdout</span><span class="p">,</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">first</span><span class="si">}</span><span class="s"> first -&gt; </span><span class="si">{</span><span class="n">out</span><span class="p">.</span><span class="n">stdout</span><span class="si">}</span><span class="sh">"</span>
</code></pre></div></div>

<h2 id="the-better-fix">The better fix</h2>

<p>Path manipulation is a workaround. The real answer is packaging: make the shared code a proper package, install it with <code class="language-plaintext highlighter-rouge">pip install -e .</code>, and let normal resolution do its job.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>project/
  pyproject.toml
  src/pipeline_common/
    __init__.py
    cost_tracker.py
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">pipeline_common</span> <span class="kn">import</span> <span class="n">cost_tracker</span>   <span class="c1"># unambiguous, order-independent
</span></code></pre></div></div>

<p>If you are past a couple of stages, do this instead of reaching for <code class="language-plaintext highlighter-rouge">sys.path</code>. Duplicate module names across a repository are a slow-burning fuse, and <code class="language-plaintext highlighter-rouge">insert(0, ...)</code> is what lights it.</p>]]></content><author><name></name></author><category term="Python" /><category term="python" /><category term="imports" /><category term="debugging" /><category term="packaging" /><summary type="html"><![CDATA[One line added a shared helper to a staged pipeline and made a completely unrelated import resolve to the wrong file, but only sometimes.]]></summary></entry><entry><title type="html">Stop cutting on the clock</title><link href="https://ifekri.github.io/posts/stop-cutting-on-the-clock/" rel="alternate" type="text/html" title="Stop cutting on the clock" /><published>2026-07-20T05:15:00-04:00</published><updated>2026-07-20T05:15:00-04:00</updated><id>https://ifekri.github.io/posts/stop-cutting-on-the-clock</id><content type="html" xml:base="https://ifekri.github.io/posts/stop-cutting-on-the-clock/"><![CDATA[<p>My scene planner divided total duration by ten and generated one image per slot. A 25-minute video produced 149 images. Simple, predictable, and wrong on both axes: it cost more than it needed to, and it cut in the middle of sentences.</p>

<!--more-->

<h2 id="the-information-was-already-there">The information was already there</h2>

<p>The transcription step produced 416 subtitle segments with word-level timings. Those are semantic boundaries — real ones, derived from where the speaker actually paused. The planner ignored all of them and used a stopwatch instead.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># What it did
</span><span class="n">scene_count</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">total_duration</span> <span class="o">/</span> <span class="mi">10</span><span class="p">)</span>
<span class="n">scenes</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nc">Scene</span><span class="p">(</span><span class="n">start</span><span class="o">=</span><span class="n">i</span> <span class="o">*</span> <span class="mi">10</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="mi">10</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">scene_count</span><span class="p">)</span>
<span class="p">]</span>
</code></pre></div></div>

<p>Every boundary here is arbitrary. Some land mid-clause. Some split a sentence whose two halves need the same image.</p>

<h2 id="group-on-meaning-bounded-by-time">Group on meaning, bounded by time</h2>

<p>Keep the duration bounds — they exist so no image sits on screen too long — but let content decide where inside them the cut falls.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">plan_scenes</span><span class="p">(</span><span class="n">segments</span><span class="p">,</span> <span class="n">min_len</span><span class="o">=</span><span class="mf">10.0</span><span class="p">,</span> <span class="n">max_len</span><span class="o">=</span><span class="mf">15.0</span><span class="p">):</span>
    <span class="n">scenes</span><span class="p">,</span> <span class="n">current</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">seg</span> <span class="ow">in</span> <span class="n">segments</span><span class="p">:</span>
        <span class="n">current</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">seg</span><span class="p">)</span>
        <span class="n">span</span> <span class="o">=</span> <span class="n">current</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">end</span> <span class="o">-</span> <span class="n">current</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">start</span>

        <span class="k">if</span> <span class="n">span</span> <span class="o">&lt;</span> <span class="n">min_len</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="c1"># Past the minimum: cut at a real boundary, or at the ceiling
</span>        <span class="k">if</span> <span class="n">seg</span><span class="p">.</span><span class="n">ends_sentence</span> <span class="ow">or</span> <span class="n">span</span> <span class="o">&gt;=</span> <span class="n">max_len</span><span class="p">:</span>
            <span class="n">scenes</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="nc">Scene</span><span class="p">(</span>
                <span class="n">start</span><span class="o">=</span><span class="n">current</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">start</span><span class="p">,</span>
                <span class="n">end</span><span class="o">=</span><span class="n">current</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">end</span><span class="p">,</span>
                <span class="n">text</span><span class="o">=</span><span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">text</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">current</span><span class="p">),</span>
            <span class="p">))</span>
            <span class="n">current</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">if</span> <span class="n">current</span><span class="p">:</span>
        <span class="n">scenes</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="nc">Scene</span><span class="p">(</span>
            <span class="n">start</span><span class="o">=</span><span class="n">current</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">start</span><span class="p">,</span>
            <span class="n">end</span><span class="o">=</span><span class="n">current</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">end</span><span class="p">,</span>
            <span class="n">text</span><span class="o">=</span><span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">text</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">current</span><span class="p">),</span>
        <span class="p">))</span>
    <span class="k">return</span> <span class="n">scenes</span>
</code></pre></div></div>

<p>The scene now carries its own text, which means the prompt for its image can be built from what is actually being said during it — not from whatever words happened to fall inside a ten-second window.</p>

<h2 id="what-it-costs">What it costs</h2>

<p>At my measured rate the arithmetic is blunt:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>images</th>
      <th>image spend</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>fixed 10s</td>
      <td>149</td>
      <td>$5.07</td>
    </tr>
    <tr>
      <td>semantic, 10-15s</td>
      <td>~60</td>
      <td>$2.04</td>
    </tr>
  </tbody>
</table>

<p>Image generation was 96% of my per-video cost. Cutting image count by 60% cuts the whole unit cost by nearly the same proportion. No other optimisation in the pipeline comes close.</p>

<h2 id="the-trap">The trap</h2>

<p>Longer scenes with static images look worse, not better. A still frame held for fifteen seconds reads as a stall. You have to spend some of what you saved on motion.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">progress</span> <span class="o">=</span> <span class="nx">frame</span> <span class="o">/</span> <span class="nx">durationInFrames</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">scale</span> <span class="o">=</span> <span class="nf">interpolate</span><span class="p">(</span><span class="nx">progress</span><span class="p">,</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="p">[</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.08</span><span class="p">]);</span>
<span class="kd">const</span> <span class="nx">x</span> <span class="o">=</span> <span class="nf">interpolate</span><span class="p">(</span><span class="nx">progress</span><span class="p">,</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">20</span><span class="p">]);</span>

<span class="p">&lt;</span><span class="nt">img</span> <span class="na">style</span><span class="p">=</span><span class="si">{</span><span class="p">{</span> <span class="na">transform</span><span class="p">:</span> <span class="s2">`scale(</span><span class="p">${</span><span class="nx">scale</span><span class="p">}</span><span class="s2">) translateX(</span><span class="p">${</span><span class="nx">x</span><span class="p">}</span><span class="s2">px)`</span> <span class="p">}</span><span class="si">}</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<p>Slow, restrained, and varied in direction between scenes so it does not become its own pattern. Roughly 8% scale over the full duration is enough — anything more reads as an effect rather than as life.</p>

<p>Ship the two changes together. Semantic grouping without motion makes the output cheaper and worse, which is the wrong trade to make on the thing you sell.</p>]]></content><author><name></name></author><category term="Pipelines" /><category term="video" /><category term="scene-planning" /><category term="cost-optimisation" /><category term="python" /><summary type="html"><![CDATA[A fixed interval is the easiest way to segment a video and the most expensive. Word-level timings you already have can do it better.]]></summary></entry><entry><title type="html">Rust CLI tools that feel native</title><link href="https://ifekri.github.io/posts/rust-cli-tools-that-feel-native/" rel="alternate" type="text/html" title="Rust CLI tools that feel native" /><published>2026-07-19T07:30:00-04:00</published><updated>2026-07-19T07:30:00-04:00</updated><id>https://ifekri.github.io/posts/rust-cli-tools-that-feel-native</id><content type="html" xml:base="https://ifekri.github.io/posts/rust-cli-tools-that-feel-native/"><![CDATA[<p>A CLI tool can be fast, correct, and still feel wrong. The difference between a tool people tolerate and a tool people reach for comes down to a handful of small decisions that most developers skip.</p>

<!--more-->

<h2 id="output-is-the-interface">Output is the interface</h2>

<p>Your CLI’s output is its UI. Treat it that way.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::{</span><span class="k">self</span><span class="p">,</span> <span class="n">Write</span><span class="p">};</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// Wrong: dumping raw data</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">results</span><span class="p">);</span>

    <span class="c1">// Right: structured, scannable output</span>
    <span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="n">results</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"  {}  {:&lt;24}  {}"</span><span class="p">,</span> <span class="n">item</span><span class="nf">.status_icon</span><span class="p">(),</span> <span class="n">item</span><span class="py">.name</span><span class="p">,</span> <span class="n">item</span><span class="py">.detail</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Always flush before exit on interactive output</span>
    <span class="nn">io</span><span class="p">::</span><span class="nf">stdout</span><span class="p">()</span><span class="nf">.flush</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Align columns. Use color to communicate state, not to decorate. Red means failure, green means success, yellow means attention. Never use color as the only signal, and always respect <code class="language-plaintext highlighter-rouge">NO_COLOR</code>.</p>

<h2 id="errors-should-teach">Errors should teach</h2>

<p>A good error message tells the user what went wrong and what to do next.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Wrong</span>
<span class="nd">eprintln!</span><span class="p">(</span><span class="s">"Error: connection failed"</span><span class="p">);</span>

<span class="c1">// Right</span>
<span class="nd">eprintln!</span><span class="p">(</span><span class="s">"error: could not reach registry.example.com:443"</span><span class="p">);</span>
<span class="nd">eprintln!</span><span class="p">(</span><span class="s">"  hint: check your network connection or VPN status"</span><span class="p">);</span>
<span class="nd">eprintln!</span><span class="p">(</span><span class="s">"  hint: run with --offline to use the local cache"</span><span class="p">);</span>
</code></pre></div></div>

<p>Exit codes matter too. Zero for success, non-zero for failure, and different codes for different failure classes so scripts can branch on them.</p>

<h2 id="flags-should-feel-inevitable">Flags should feel inevitable</h2>

<p>Follow the conventions users already know:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-h</code> / <code class="language-plaintext highlighter-rouge">--help</code> for help</li>
  <li><code class="language-plaintext highlighter-rouge">-v</code> / <code class="language-plaintext highlighter-rouge">--verbose</code> for more output</li>
  <li><code class="language-plaintext highlighter-rouge">-q</code> / <code class="language-plaintext highlighter-rouge">--quiet</code> for less output</li>
  <li><code class="language-plaintext highlighter-rouge">--version</code> for version info</li>
  <li><code class="language-plaintext highlighter-rouge">--dry-run</code> when the tool changes state</li>
</ul>

<p>Use <code class="language-plaintext highlighter-rouge">clap</code> with derive macros. The help output it generates is better than anything you’ll write by hand.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="nd">#[derive(Parser)]</span>
<span class="nd">#[command(name</span> <span class="nd">=</span> <span class="s">"wiretap"</span><span class="nd">,</span> <span class="nd">about</span> <span class="nd">=</span> <span class="s">"Inspect HTTP traffic from the terminal"</span><span class="nd">)]</span>
<span class="k">struct</span> <span class="n">Args</span> <span class="p">{</span>
    <span class="cd">/// Port to listen on</span>
    <span class="nd">#[arg(short,</span> <span class="nd">long,</span> <span class="nd">default_value_t</span> <span class="nd">=</span> <span class="mi">8080</span><span class="nd">)]</span>
    <span class="n">port</span><span class="p">:</span> <span class="nb">u16</span><span class="p">,</span>

    <span class="cd">/// Filter by route pattern</span>
    <span class="nd">#[arg(short,</span> <span class="nd">long)]</span>
    <span class="n">filter</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">,</span>

    <span class="cd">/// Output raw JSON instead of formatted text</span>
    <span class="nd">#[arg(long)]</span>
    <span class="n">json</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="speed-is-a-feature">Speed is a feature</h2>

<p>Users notice startup time. If your tool takes 400ms to print help, it feels broken. Keep the binary small, avoid lazy network calls at startup, and profile the cold path.</p>

<p>The best CLI tools feel instant. That’s not an accident. It’s a design decision.</p>]]></content><author><name></name></author><category term="Tooling" /><category term="rust" /><category term="cli" /><category term="developer-experience" /><summary type="html"><![CDATA[The small design decisions that make a terminal tool feel like it belongs.]]></summary></entry><entry><title type="html">Read the source, not the warning</title><link href="https://ifekri.github.io/posts/read-the-source-not-the-warning/" rel="alternate" type="text/html" title="Read the source, not the warning" /><published>2026-07-13T08:20:00-04:00</published><updated>2026-07-13T08:20:00-04:00</updated><id>https://ifekri.github.io/posts/read-the-source-not-the-warning</id><content type="html" xml:base="https://ifekri.github.io/posts/read-the-source-not-the-warning/"><![CDATA[<p>My renderer printed this on every run:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hardware accelerated encoding disabled - "crf" option is not
supported with hardware acceleration
</code></pre></div></div>

<p>The obvious reading: my CRF value is too aggressive, lower it and hardware encoding comes back. I built a task around that assumption and wrote it into project documentation. Both were wrong.</p>

<!--more-->

<h2 id="twenty-minutes-in-node_modules">Twenty minutes in node_modules</h2>

<p>Before spending a day on it, I opened the file that emits the warning.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @remotion/renderer/dist/get-codec-name.js</span>
<span class="k">if </span><span class="p">(</span><span class="nx">crf</span> <span class="o">!==</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="k">typeof</span> <span class="nx">crf</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="dl">'</span><span class="s1">crf</span><span class="dl">'</span><span class="p">;</span>        <span class="c1">// -&gt; disables hardware acceleration</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It is not the value. Specifying <code class="language-plaintext highlighter-rouge">crf</code> <strong>at all</strong> trips this branch. Any number, including the “safe” one I was about to switch to.</p>

<p>Then, a few lines down:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if </span><span class="p">(</span><span class="nx">codec</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">h264</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">preferred</span> <span class="o">&amp;&amp;</span> <span class="nx">process</span><span class="p">.</span><span class="nx">platform</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">darwin</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">unsupportedQualityOption</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span> <span class="na">encoderName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">h264_videotoolbox</span><span class="dl">'</span><span class="p">,</span> <span class="na">hardwareAccelerated</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span>
    <span class="p">}</span>
    <span class="nf">warnAboutDisabledHardwareAcceleration</span><span class="p">();</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">encoderName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">libx264</span><span class="dl">'</span><span class="p">,</span> <span class="na">hardwareAccelerated</span><span class="p">:</span> <span class="kc">false</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The only h264 hardware path is Apple VideoToolbox. On Linux and Windows it returns <code class="language-plaintext highlighter-rouge">libx264</code> unconditionally. Dropping <code class="language-plaintext highlighter-rouge">crf</code> entirely would silence the warning and change nothing, and my deployment target has no GPU regardless.</p>

<p>Three assumptions, all wrong, all disproved by reading forty lines.</p>

<h2 id="confirm-empirically-anyway">Confirm empirically anyway</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>crf 10:  exit=0  elapsed=95s  size=53,458,834  warning: YES
crf 18:  exit=0  elapsed=94s  size=27,976,310  warning: YES
</code></pre></div></div>

<p>The warning stays either way. But the change was still worth making for a reason unrelated to the warning: 1.91x smaller files at identical render time. I kept it, and documented that hardware encoding is unavailable on this platform so nobody chases it again.</p>

<h2 id="why-this-generalises">Why this generalises</h2>

<p>Warning text is written by someone describing a condition, not prescribing your fix. It tells you what the code observed. It does not tell you what branch you are in, what platform gates apply, or whether the suggested remedy exists in your environment.</p>

<p>The tools are unglamorous and fast:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"hardware acceleration"</span> node_modules/@remotion/renderer/dist/ | <span class="nb">head
</span>python <span class="nt">-c</span> <span class="s2">"import somelib, os; print(os.path.dirname(somelib.__file__))"</span>
</code></pre></div></div>

<p>Installed source is right there. It is the actual behaviour rather than a description of it, it matches your exact version rather than the docs for a different one, and reading it usually takes less time than one wrong experiment.</p>

<h2 id="the-part-that-stung">The part that stung</h2>

<p>I had written the wrong conclusion into a project brief, where it shaped a task list. A confident wrong note in shared documentation costs more than no note, because it stops other people from checking.</p>

<p>The fix was to correct it in place and add a line saying the premise had been tested and disproved — so the next person reads the finding rather than repeating the investigation.</p>]]></content><author><name></name></author><category term="Debugging" /><category term="debugging" /><category term="node" /><category term="ffmpeg" /><category term="video" /><category term="methodology" /><summary type="html"><![CDATA[A warning told me to change a setting. Changing it did nothing. Twenty minutes in node_modules explained why, and saved a week of chasing it.]]></summary></entry><entry><title type="html">Deploy pipelines that don’t wake you up</title><link href="https://ifekri.github.io/posts/deploy-pipelines-that-dont-wake-you-up/" rel="alternate" type="text/html" title="Deploy pipelines that don’t wake you up" /><published>2026-07-08T03:00:00-04:00</published><updated>2026-07-08T03:00:00-04:00</updated><id>https://ifekri.github.io/posts/deploy-pipelines-that-dont-wake-you-up</id><content type="html" xml:base="https://ifekri.github.io/posts/deploy-pipelines-that-dont-wake-you-up/"><![CDATA[<p>The best deploy pipeline is the one you never think about. Code merges, tests run, artifacts build, production updates, and nobody gets paged. That’s the goal. Everything else is a compromise.</p>

<!--more-->

<h2 id="the-failure-mode-that-matters">The failure mode that matters</h2>

<p>Most pipelines fail in the same way: a change passes CI, lands in production, and breaks something that CI never tested. The fix is not more tests. The fix is a pipeline that assumes failure and routes around it.</p>

<h2 id="three-rules">Three rules</h2>

<p><strong>1. Every deploy is reversible in one step.</strong> If rolling back requires a meeting, your pipeline is broken. Rollback should be a button, not a procedure.</p>

<p><strong>2. Health checks gate the rollout.</strong> A deploy that reports success while the app is crash-looping is a liar. The pipeline should verify the service is actually healthy before it calls the job done.</p>

<p><strong>3. Small batches beat big bangs.</strong> Ten small deploys a day is safer than one large deploy a week. The blast radius of each change stays small, and the cause of any failure is obvious.</p>

<h2 id="a-minimal-gitlab-pipeline">A minimal GitLab pipeline</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">stages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">test</span>
  <span class="pi">-</span> <span class="s">build</span>
  <span class="pi">-</span> <span class="s">deploy</span>
  <span class="pi">-</span> <span class="s">verify</span>

<span class="na">test</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">test</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">bundle exec rspec</span>

<span class="na">build</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">build</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">docker build -t app:$CI_COMMIT_SHORT_SHA .</span>
    <span class="pi">-</span> <span class="s">docker push registry.example.com/app:$CI_COMMIT_SHORT_SHA</span>

<span class="na">deploy</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">./scripts/deploy.sh $CI_COMMIT_SHORT_SHA</span>
  <span class="na">environment</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">production</span>

<span class="na">verify</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">verify</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">./scripts/healthcheck.sh || ./scripts/rollback.sh</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">verify</code> stage is the part most teams skip. It’s also the part that matters most. If the health check fails, the pipeline rolls back automatically and alerts the team. No human required.</p>

<h2 id="what-healthy-means">What “healthy” means</h2>

<p>A health check is not a 200 response from <code class="language-plaintext highlighter-rouge">/</code>. It’s a check that the service can actually do its job: connect to the database, reach its dependencies, and respond within a latency budget.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># healthcheck.sh</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>1..12<span class="o">}</span><span class="p">;</span> <span class="k">do
  if </span>curl <span class="nt">-sf</span> <span class="nt">--max-time</span> 5 https://app.example.com/health/deep <span class="o">&gt;</span> /dev/null<span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"healthy"</span>
    <span class="nb">exit </span>0
  <span class="k">fi
  </span><span class="nb">sleep </span>10
<span class="k">done

</span><span class="nb">echo</span> <span class="s2">"unhealthy after 2 minutes"</span>
<span class="nb">exit </span>1
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">/health/deep</code> endpoint checks the database connection, the cache, and any critical downstream services. A shallow ping tells you the process is alive. A deep check tells you the service is working.</p>

<h2 id="the-real-lesson">The real lesson</h2>

<p>Pipelines are not about automation. They’re about trust. When the team trusts the pipeline, they deploy often, they take smaller risks, and they sleep through the night. When they don’t, they batch changes, delay releases, and get woken up anyway.</p>

<p>Build the pipeline you’d trust at 3am. Then never get woken up by it.</p>]]></content><author><name></name></author><category term="Infrastructure" /><category term="ci-cd" /><category term="deploy" /><category term="reliability" /><summary type="html"><![CDATA[How I design CI/CD so a bad deploy is a non-event, not a 3am incident.]]></summary></entry></feed>