<?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: Kunal</title>
    <description>The latest articles on DEV Community by Kunal (@kunal_d6a8fea2309e1571ee7).</description>
    <link>https://dev.to/kunal_d6a8fea2309e1571ee7</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%2F2621382%2Fc94c296d-7804-4c0c-accc-b8f5900821ac.jpg</url>
      <title>DEV Community: Kunal</title>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kunal_d6a8fea2309e1571ee7"/>
    <language>en</language>
    <item>
      <title>How to Reduce Rust Compile Time [2026] (sccache + mold)</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:44:24 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/how-to-reduce-rust-compile-time-2026-sccache-mold-28h7</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/how-to-reduce-rust-compile-time-2026-sccache-mold-28h7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/reduce-rust-compile-time" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  How to Reduce Rust Compile Time &lt;a href="https://dev.tosccache%20+%20mold"&gt;2026&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;If you take one thing from this post, let it be this: you can &lt;strong&gt;reduce Rust compile time&lt;/strong&gt; without cargo-culting flags. You need a harness. A few numbers you can trust. Then you change one variable, re-run, and decide if it was worth the churn.&lt;/p&gt;

&lt;p&gt;You’ll finish this guide with a repeatable setup that tells you, in minutes, whether you actually reduced compile time. Not “it feels faster”. Real numbers: cold build time, warm build time, incremental rebuild time after a one-line edit, &lt;code&gt;sccache&lt;/code&gt; hit rate, and linker time.&lt;/p&gt;

&lt;p&gt;If you run a Rust monorepo in 2026, compile time isn’t a “developer experience” nit. It’s a CI bill. It’s also a throughput cap. When builds are slow, everything gets worse: smaller refactors stop happening, reviews batch up, and people start treating the build as an unpredictable weather system.&lt;/p&gt;

&lt;p&gt;Here’s my stance: &lt;strong&gt;stop chasing random Cargo knobs&lt;/strong&gt;. Treat compile time like observability. Measure the build graph, then change one thing at a time.&lt;/p&gt;

&lt;p&gt;Before we start: the measurement mindset here is the same one I use for cost and performance work on this site. I maintain a live LLM pricing tracker at &lt;a href="https://dev.to/llm-prices"&gt;/llm-prices&lt;/a&gt;, and the main lesson from that project is that &lt;em&gt;numbers without assumptions are propaganda&lt;/em&gt;. Build time “improvements” are the same. If you don’t separate cold vs warm vs incremental, you’re lying to yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Rust compile time optimization?
&lt;/h2&gt;

&lt;p&gt;Rust compile time optimization is the set of techniques that reduce the elapsed time from &lt;code&gt;cargo build&lt;/code&gt; to a usable binary by improving build graph parallelism, avoiding unnecessary recompilation via incremental builds and caching, and shrinking expensive compiler and linker work.&lt;/p&gt;

&lt;p&gt;In practice, you’re optimizing three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The crate dependency graph&lt;/strong&gt; (parallelism and invalidation boundaries)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse&lt;/strong&gt; (incremental compilation + local/remote compilation cache)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linking&lt;/strong&gt; (often the last big serial step)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A measurable harness to reduce Rust compile time
&lt;/h2&gt;

&lt;p&gt;You can’t optimize something you can’t re-run. The fastest way to waste a week is to “try a bunch of stuff” and then argue in Slack about whether it helped.&lt;/p&gt;

&lt;p&gt;So I always start with a tiny benchmark script and a rule: same target, same features, same toolchain, same environment. Every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 5-metric baseline
&lt;/h3&gt;

&lt;p&gt;Run these and record them in your PR description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold build&lt;/strong&gt;: clean build from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm build&lt;/strong&gt;: immediate rebuild with no changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental rebuild&lt;/strong&gt;: rebuild after a one-line edit in a leaf crate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sccache&lt;/code&gt; hit rate&lt;/strong&gt;: hits / requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link time&lt;/strong&gt;: seconds spent in linking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete targets that usually matter in real teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold build down by &lt;strong&gt;20–50%&lt;/strong&gt; is “worth telling the org”.&lt;/li&gt;
&lt;li&gt;Warm build under &lt;strong&gt;5s&lt;/strong&gt; is where iteration stops feeling sticky.&lt;/li&gt;
&lt;li&gt;Incremental rebuild under &lt;strong&gt;1–2s&lt;/strong&gt; for leaf edits is the happy place.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sccache&lt;/code&gt; hit rate above &lt;strong&gt;70%&lt;/strong&gt; in CI is where it starts paying for itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re not sure what to target, start by picking the one that’s actively annoying you.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I separate cold build vs warm/incremental build measurements?
&lt;/h3&gt;

&lt;p&gt;Do it explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold: &lt;code&gt;cargo clean&lt;/code&gt; then &lt;code&gt;cargo build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Warm: run &lt;code&gt;cargo build&lt;/code&gt; again with no edits&lt;/li&gt;
&lt;li&gt;Incremental: edit one file in one crate, rebuild once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the environment constant: same toolchain, same target, same &lt;code&gt;RUSTFLAGS&lt;/code&gt;, same feature set.&lt;/p&gt;

&lt;p&gt;If you’re in a workspace, always build the same thing. “I ran &lt;code&gt;cargo build&lt;/code&gt;” is not a stable benchmark if half the time you’re building examples/tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add build graph visibility: cargo build --timings
&lt;/h3&gt;

&lt;p&gt;Cargo timings is the first diagnostic tool I reach for because it forces the conversation into reality. It generates an &lt;strong&gt;HTML Gantt chart&lt;/strong&gt; that shows crate dependencies and how much parallelism you’re actually getting.&lt;/p&gt;

&lt;p&gt;Nicholas Nethercote (Mozilla alum) has the best practical write-up I’ve seen on this whole topic. Read it. Then come back. &lt;a href="https://nnethercote.github.io/perf-book/compile-times.html" rel="noopener noreferrer"&gt;Nicholas Nethercote&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo build --timings&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open the generated HTML. The “aha” you’re looking for is usually boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One crate eating &lt;strong&gt;30–60%&lt;/strong&gt; of the total build.&lt;/li&gt;
&lt;li&gt;A long serialized chain where only &lt;strong&gt;1&lt;/strong&gt; crate is compiling at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not a “Rust is slow” problem. That’s your crate graph telling you it’s shaped badly.&lt;/p&gt;

&lt;h3&gt;
  
  
  A snippet-friendly checklist (print this)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Generate a &lt;code&gt;cargo build --timings&lt;/code&gt; report and identify the top &lt;strong&gt;3&lt;/strong&gt; crates by time.&lt;/li&gt;
&lt;li&gt;Split any crate that serializes the graph (or move heavy code behind a feature).&lt;/li&gt;
&lt;li&gt;Turn on incremental in dev, and keep release separate.&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;sccache&lt;/code&gt;, verify hit rate, then add remote caching for CI.&lt;/li&gt;
&lt;li&gt;Swap the linker to &lt;code&gt;mold&lt;/code&gt; on Linux and measure link time again.&lt;/li&gt;
&lt;li&gt;Trim features and optional dependencies until the timing report changes.&lt;/li&gt;
&lt;li&gt;Hunt macro and IR bloat in the top crates (&lt;code&gt;-Zmacro-stats&lt;/code&gt;, &lt;code&gt;cargo llvm-lines&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Visualization (cargo build --timings): find the crates that dominate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I measure which crates dominate my Rust build time?
&lt;/h3&gt;

&lt;p&gt;Use the timings report, not vibes.&lt;/p&gt;

&lt;p&gt;The timings HTML gives you per-crate durations. Take the top offenders and write them down. If one crate is &lt;strong&gt;9s&lt;/strong&gt; of a &lt;strong&gt;14s&lt;/strong&gt; build, you already know where to spend your next hour.&lt;/p&gt;

&lt;p&gt;Two patterns show up constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The “kitchen sink” crate&lt;/strong&gt;: one crate owns half the domain model, half the macros, half the dependencies. It blocks the whole build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The proc-macro chokepoint&lt;/strong&gt;: everything depends on a proc-macro crate, so compilation waits for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast builds come from wide graphs. Narrow graphs feel “clean” right up until you have to work in them.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I split a workspace/crate to increase parallelism, and what are the tradeoffs?
&lt;/h3&gt;

&lt;p&gt;Split when the timings report shows a crate that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;takes &lt;strong&gt;&amp;gt;25%&lt;/strong&gt; of build time, and&lt;/li&gt;
&lt;li&gt;sits early in the dependency chain (lots of crates depend on it).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tradeoffs (the real ones):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More crates means more &lt;code&gt;Cargo.toml&lt;/code&gt; surface area.&lt;/li&gt;
&lt;li&gt;You can accidentally increase compile time if you create lots of tiny crates that each pull in the same heavy deps.&lt;/li&gt;
&lt;li&gt;API boundaries harden. That’s good for architecture, but it can feel annoying in early-stage products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule I follow: split by &lt;strong&gt;invalidation boundaries&lt;/strong&gt;. If code changes together, keep it together. If code changes independently, make it a crate.&lt;/p&gt;

&lt;p&gt;For example, an “api-types” crate that changes every PR is a terrible foundation crate. Push stable things down (core traits, shared error types), and pull unstable things up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cargo profile settings (incremental, lto, codegen-units)
&lt;/h2&gt;

&lt;p&gt;Cargo profiles are where you separate “fast local iteration” from “optimized release binary”. Profiles control compiler settings like incremental, LTO, codegen units, debug info, and assertions. The canonical reference is the &lt;a href="https://doc.rust-lang.org/cargo/reference/profiles.html" rel="noopener noreferrer"&gt;Rust Project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One thing I’ll say out loud because teams keep messing this up: if you optimize for release builds at the expense of dev iteration, you pay for it daily. Don’t do that. Make release expensive on purpose. Make dev cheap on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Cargo profile knobs most affect compile time for dev vs release?
&lt;/h3&gt;

&lt;p&gt;The knobs that actually move build time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;incremental&lt;/code&gt;: big dev win, sometimes noisy in CI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opt-level&lt;/code&gt;: higher is slower. &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt; are usually the sweet spot for dev.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lto&lt;/code&gt;: can add minutes on large binaries.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;codegen-units&lt;/code&gt;: more units can improve parallelism but can change performance and compile time tradeoffs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debug&lt;/code&gt;: full debug info can be expensive. Line-tables-only is often enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sane baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dev&lt;/code&gt;: prioritize iteration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release&lt;/code&gt;: prioritize runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team is shipping often, add a middle profile like &lt;code&gt;release-fast&lt;/code&gt; for staging builds.&lt;/p&gt;

&lt;p&gt;Concrete numbers that matter: on a typical service binary, flipping &lt;code&gt;lto = true&lt;/code&gt; can turn a &lt;strong&gt;30s&lt;/strong&gt; release build into &lt;strong&gt;90–180s&lt;/strong&gt;, depending on code size. That might be fine for nightly. It’s usually not fine for every PR.&lt;/p&gt;

&lt;h3&gt;
  
  
  What invalidates incremental compilation most often, and how do I reduce dep-graph churn?
&lt;/h3&gt;

&lt;p&gt;Incremental compilation works when edits stay local. It falls apart when your changes force large parts of the dependency graph to be reconsidered.&lt;/p&gt;

&lt;p&gt;The churn culprits that tend to be brutal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editing a foundational crate that &lt;strong&gt;dozens&lt;/strong&gt; depend on.&lt;/li&gt;
&lt;li&gt;Re-export patterns that make “small change” look “global”.&lt;/li&gt;
&lt;li&gt;Heavy macro-generated code that changes in big chunks.&lt;/li&gt;
&lt;li&gt;Build scripts (&lt;code&gt;build.rs&lt;/code&gt;) that embed timestamps or environment-dependent outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mitigations you can actually ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep “core” crates stable. Move fast-changing code upward.&lt;/li&gt;
&lt;li&gt;Avoid needless &lt;code&gt;pub use&lt;/code&gt; fan-out in a root crate.&lt;/li&gt;
&lt;li&gt;Make build scripts deterministic. If a script reads the filesystem, lock it down.&lt;/li&gt;
&lt;li&gt;Trim features so fewer crates participate in the build in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  sccache installation and usage (local + CI)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sccache&lt;/code&gt; is a compiler wrapper that avoids recompilation when possible and can use local or remote cache backends. That’s straight from the project README: &lt;a href="https://github.com/mozilla/sccache" rel="noopener noreferrer"&gt;Mozilla&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The mistake people make is treating &lt;code&gt;sccache&lt;/code&gt; like a magic on-switch. It’s not. It’s a multiplier. If your build is nondeterministic or your graph invalidates constantly, &lt;code&gt;sccache&lt;/code&gt; can’t save you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install sccache locally
&lt;/h3&gt;

&lt;p&gt;If you’re already in Rust land, the simplest path is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo install sccache&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then set the wrapper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;export RUSTC_WRAPPER=sccache&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify it’s active:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sccache --version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sccache --show-stats&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stat you care about is “Compile requests” vs “Cache hits”. After a second build, you should see hits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure sccache for Rust in CI (remote cache)
&lt;/h3&gt;

&lt;p&gt;Local caching is nice. Remote caching is what moves your CI bill.&lt;/p&gt;

&lt;p&gt;The basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick a remote backend your org already trusts (S3, Redis, etc.).&lt;/li&gt;
&lt;li&gt;Ensure the cache key is stable across machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common reason CI hit rates stay awful is path differences. You check out the repo under &lt;code&gt;/home/runner/work/...&lt;/code&gt; in CI and &lt;code&gt;/Users/kunal/...&lt;/code&gt; locally, and the compiler bakes paths into artifacts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sccache&lt;/code&gt; supports path normalization via &lt;code&gt;SCCACHE_BASEDIR&lt;/code&gt; / base-dir normalization options (see the README section on normalizing paths). If you don’t normalize, expecting &amp;gt;70% hit rate across ephemeral runners is wishful thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I verify cache hit rate?
&lt;/h3&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sccache --show-stats&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Track it as a metric.&lt;/p&gt;

&lt;p&gt;If your CI does 200 compile requests and gets 20 hits, that’s a &lt;strong&gt;10%&lt;/strong&gt; hit rate. You don’t have a caching system. You have a warm feeling.&lt;/p&gt;

&lt;h3&gt;
  
  
  When does sccache not help, and how do I mitigate?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sccache&lt;/code&gt; won’t save you when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your code changes invalidate everything.&lt;/li&gt;
&lt;li&gt;You rely heavily on procedural macros that themselves change often.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build.rs&lt;/code&gt; scripts emit different outputs per machine.&lt;/li&gt;
&lt;li&gt;Your environment leaks into compilation (paths, env vars, feature flags).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mitigation playbook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normalize paths.&lt;/li&gt;
&lt;li&gt;Freeze toolchains (pin Rust version in CI).&lt;/li&gt;
&lt;li&gt;Make builds deterministic (lock down build scripts).&lt;/li&gt;
&lt;li&gt;Trim features so fewer crates are in the build graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do all that and still see low hit rates, don’t get religious about caching. Fix the graph and the invalidation boundaries first.&lt;/p&gt;

&lt;h2&gt;
  
  
  mold installation and usage: cut link time (Linux-first)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mold&lt;/code&gt; is designed to be a faster, drop-in replacement for existing Unix linkers. That’s the pitch, and it mostly delivers, per &lt;a href="https://github.com/rui314/mold" rel="noopener noreferrer"&gt;Rui Ueyama&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Linking is often the most annoying part of a Rust build because it’s serial. You can compile crates across 16 cores. Then you hit the link step and watch one core do all the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install mold
&lt;/h3&gt;

&lt;p&gt;On Linux, you typically install &lt;code&gt;mold&lt;/code&gt; via your package manager (or build from source).&lt;/p&gt;

&lt;p&gt;Quick sanity check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mold --version&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use mold with Cargo / rustc
&lt;/h3&gt;

&lt;p&gt;There are a few ways to do this; the simplest is setting a linker override via &lt;code&gt;RUSTFLAGS&lt;/code&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RUSTFLAGS="-C link-arg=-fuse-ld=mold" cargo build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Exact flags vary by toolchain and platform. The point is: make Cargo use &lt;code&gt;mold&lt;/code&gt; as the linker.)&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I confirm link time improved?
&lt;/h3&gt;

&lt;p&gt;Go back to the timings report.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;cargo build --timings&lt;/code&gt; before.&lt;/li&gt;
&lt;li&gt;Switch to &lt;code&gt;mold&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;cargo build --timings&lt;/code&gt; again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’re looking for the link step dropping from something like &lt;strong&gt;4–8s&lt;/strong&gt; to &lt;strong&gt;1–3s&lt;/strong&gt; on medium-sized binaries. On bigger monorepos, link time can be &lt;strong&gt;10s+&lt;/strong&gt;. Those are the wins that change how the repo feels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform nuance (macOS + Windows)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: &lt;code&gt;mold&lt;/code&gt; is the straightforward win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: you’re constrained by Apple’s linker ecosystem. You can still reduce link work by trimming debug info and avoiding pathological dependency graphs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: linker behavior is different again. Don’t assume a Linux linker swap translates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure it. If it didn’t move, undo the change and move on.&lt;/p&gt;

&lt;p&gt;Here’s the official demo-style video if you want the quick version before you change a bunch of build plumbing:&lt;/p&gt;

&lt;p&gt;[YOUTUBE:NE4Exqt_0z8|Rust Compiles Faster NOW!]&lt;/p&gt;

&lt;h2&gt;
  
  
  Macros (-Zmacro-stats) and LLVM IR bloat: find compile-time blowups
&lt;/h2&gt;

&lt;p&gt;Once you’ve squeezed the “graph and tools” wins, the remaining pain is usually self-inflicted. Macro explosion, generic explosion, or both.&lt;/p&gt;

&lt;p&gt;Nicholas Nethercote recommends using the nightly &lt;code&gt;-Zmacro-stats&lt;/code&gt; flag to quantify how much code macros generate. See &lt;a href="https://nnethercote.github.io/perf-book/compile-times.html" rel="noopener noreferrer"&gt;Nicholas Nethercote&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What code patterns cause compile-time blowups, and how do I find them?
&lt;/h3&gt;

&lt;p&gt;Two common offenders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Procedural macros&lt;/strong&gt; generating code on the same order of magnitude as your hand-written code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generics/monomorphization&lt;/strong&gt; creating lots of instantiations, ballooning LLVM IR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How to find macro bloat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RUSTFLAGS="-Zmacro-stats" cargo +nightly build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Numbers to watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a proc macro generates &lt;strong&gt;200k lines&lt;/strong&gt; of expanded code, that’s not “just build tooling”. That’s a build-time tax you chose.&lt;/li&gt;
&lt;li&gt;If expanded code is &lt;strong&gt;1x&lt;/strong&gt; your handwritten code, you should at least question it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How to find LLVM IR bloat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo llvm-lines&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generic functions are often top offenders because they can be instantiated &lt;strong&gt;dozens or hundreds&lt;/strong&gt; of times in large programs. If a single generic function produces a disproportionate amount of IR, make it smaller or reduce the generic surface area.&lt;/p&gt;

&lt;p&gt;This is also where feature trimming pays off. If you can compile &lt;strong&gt;30% fewer&lt;/strong&gt; crates, you often avoid compiling the most macro-heavy optional stuff entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature flag trimming: the most underrated lever
&lt;/h3&gt;

&lt;p&gt;Optional dependencies are not optional to your compile time if you compile with default features everywhere.&lt;/p&gt;

&lt;p&gt;A safe workflow I like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run timings with your current features.&lt;/li&gt;
&lt;li&gt;Disable default features on heavy dependencies.&lt;/li&gt;
&lt;li&gt;Add back only what you need, crate-by-crate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If this sounds like work, it is. But it’s clean work. And it pays you back every single day you touch the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  A before/after table (what “good” looks like)
&lt;/h2&gt;

&lt;p&gt;Use a table like this in your repo. Fill it with your own numbers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Cold build&lt;/th&gt;
&lt;th&gt;Warm build&lt;/th&gt;
&lt;th&gt;1-line edit rebuild&lt;/th&gt;
&lt;th&gt;Link time&lt;/th&gt;
&lt;th&gt;sccache hit rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;14.2s&lt;/td&gt;
&lt;td&gt;6.1s&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;td&gt;5.2s&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ incremental dev profile&lt;/td&gt;
&lt;td&gt;14.2s&lt;/td&gt;
&lt;td&gt;5.4s&lt;/td&gt;
&lt;td&gt;1.9s&lt;/td&gt;
&lt;td&gt;5.2s&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ sccache local&lt;/td&gt;
&lt;td&gt;14.2s&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;5.2s&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ sccache remote (CI)&lt;/td&gt;
&lt;td&gt;14.2s&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;5.2s&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ mold&lt;/td&gt;
&lt;td&gt;14.2s&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;1.9s&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those numbers are illustrative. The important thing is the shape: caching and linker swaps should show up as obvious step changes. If they don’t, your config isn’t working.&lt;/p&gt;

&lt;p&gt;One more data anchor, since this blog is obsessed with measurable engineering: based on the LLM pricing tracker I maintain at &lt;a href="https://dev.to/llm-prices"&gt;/llm-prices&lt;/a&gt;, small per-unit costs compound brutally at scale. CI minutes are the same. If your org runs &lt;strong&gt;500&lt;/strong&gt; builds/day and you shave &lt;strong&gt;60s&lt;/strong&gt; off each, that’s &lt;strong&gt;8.3 hours&lt;/strong&gt; of compute time saved every single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  My opinionated ordering (what to do Monday morning)
&lt;/h2&gt;

&lt;p&gt;If you’re trying to reduce Rust compile time in a real codebase, here’s the order I’d do it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate timings and identify the top 3 crates.&lt;/li&gt;
&lt;li&gt;Fix the crate graph (split, move code, isolate proc-macros).&lt;/li&gt;
&lt;li&gt;Turn on incremental for dev. Keep release separate.&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;sccache&lt;/code&gt; locally. Verify hits.&lt;/li&gt;
&lt;li&gt;Add remote &lt;code&gt;sccache&lt;/code&gt; in CI and chase hit rate.&lt;/li&gt;
&lt;li&gt;Swap to &lt;code&gt;mold&lt;/code&gt; on Linux. Measure link time.&lt;/li&gt;
&lt;li&gt;Hunt macro/IR bloat only in the crates that show up at the top.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else is busywork.&lt;/p&gt;

&lt;p&gt;If you want the broader theme behind this, I wrote a metrics-first piece on observability here: &lt;a href="https://dev.to/blog/llm-observability-metrics"&gt;How to Pick LLM Application Observability Metrics [2026]&lt;/a&gt;. Same energy. Different bottleneck.&lt;/p&gt;

&lt;p&gt;And if compile time pain is showing up as workflow friction and review latency, you’ll probably also like my copy-paste process for &lt;a href="https://dev.to/blog/stacked-prs-github-workflow"&gt;Stacked PRs on GitHub&lt;/a&gt; and the CI framing in &lt;a href="https://dev.to/blog/ai-code-review-github-actions"&gt;AI Code Review in Your CI/CD Pipeline&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My prediction: by the end of 2026, the teams that feel “fast” won’t be the ones with the fanciest codegen or the most magical build system. They’ll be the ones that put &lt;strong&gt;build time&lt;/strong&gt; on the same dashboard as test duration and deploy frequency, and treat the crate graph like architecture, not an accident.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/reduce-rust-compile-time?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=reduce-rust-compile-time" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>buildperformance</category>
      <category>tooling</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Bun vs Node Performance 2026: Cold Start, Memory, WebSockets</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Sun, 16 Aug 2026 00:42:20 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/bun-vs-node-performance-2026-cold-start-memory-websockets-3lm8</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/bun-vs-node-performance-2026-cold-start-memory-websockets-3lm8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/bun-vs-node-performance-2026" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You’re not deciding between Bun and Node for “developer happiness.” You’re deciding whether your next service will autoscale cleanly, fit 2× more pods per node, and keep 50,000 WebSocket connections from turning into a permanent CPU tax.&lt;/p&gt;

&lt;p&gt;If you’re searching &lt;strong&gt;bun vs node performance 2026&lt;/strong&gt;, you probably already know the hot takes. Bun is “fast.” Node is “mature.” Cool. That doesn’t help when you’re staring at p95 cold starts, tight memory limits, and a real-time feature that suddenly made your fleet stateful.&lt;/p&gt;

&lt;p&gt;Here’s what actually matters in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold start is the most honest benchmark for modern infra.&lt;/strong&gt; It’s what autoscaling and serverless feel like when nobody’s watching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory isn’t &lt;code&gt;heapUsed&lt;/code&gt;.&lt;/strong&gt; RSS decides container density and whether Kubernetes evicts you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets are where runtimes get exposed.&lt;/strong&gt; Churn, backpressure, and broadcast patterns punish sloppy event loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun can be a clean win, but only in specific service shapes.&lt;/strong&gt; Greenfield HTTP + WS services benefit the most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node still wins by default when your app depends on the ecosystem.&lt;/strong&gt; LTS cadence, native addons, and tooling depth matter in boring but expensive ways.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If your workload lives on autoscaling and long-lived connections, runtime choice is an infrastructure decision, not a language decision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is Bun vs Node performance 2026?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bun vs Node performance 2026 is the real-world comparison of Bun’s JavaScriptCore-based runtime and Node.js’s V8-based runtime across the metrics that actually hit production: cold start latency, memory footprint (RSS), and WebSocket throughput/connection handling.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When people argue about “performance,” they usually mean throughput on a microbenchmark. In 2026, most teams feel performance as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tail latency&lt;/strong&gt; during scale events (cold start p95)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binpacking pressure&lt;/strong&gt; (RSS per replica)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent connection cost&lt;/strong&gt; (WebSockets: CPU per 10k conns, fan-out capacity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the shape of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark harness I’d trust (and what “fair” means)
&lt;/h2&gt;

&lt;p&gt;Most runtime shootouts cheat accidentally.&lt;/p&gt;

&lt;p&gt;They benchmark “hello world” with no reverse proxy, no TLS, no connection limits, no warmup phase. Then they act shocked when the results don’t map to production. I don’t care if a runtime wins a toy test by 12%. I care if it lets me run fewer nodes or stop flirting with OOMKills.&lt;/p&gt;

&lt;p&gt;Here’s the harness design that stays defensible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workloads (three, because production has three kinds of pain)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cold start (containers/serverless style):&lt;/strong&gt; start process, bind port, serve first request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady-state memory:&lt;/strong&gt; idle RSS and RSS under load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection churn:&lt;/strong&gt; connect/disconnect loops (LB reconnections, mobile clients)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-lived connections:&lt;/strong&gt; 5–60 minutes stable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Echo vs broadcast fan-out:&lt;/strong&gt; “send back to caller” is not what chat systems do&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concrete defaults I’d use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP JSON endpoint:&lt;/strong&gt; 1 KB JSON request, 2 KB JSON response, basic validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket message size:&lt;/strong&gt; 256 B, 1 KB, 8 KB (three tiers that match chat, presence, and “oops we shipped JSON over WS”).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rooms:&lt;/strong&gt; 100 rooms, 1,000 rooms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clients:&lt;/strong&gt; 1,000 / 10,000 / 50,000 connections (stepwise, because failure modes change).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fairness controls (the stuff everyone “forgets”)
&lt;/h3&gt;

&lt;p&gt;If you don’t lock these down, you’re not benchmarking runtimes. You’re benchmarking your own sloppiness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same machine, same kernel, same ulimits.&lt;/strong&gt; If one run gets a different &lt;code&gt;ulimit -n&lt;/code&gt;, your WebSocket result is a joke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU pinning:&lt;/strong&gt; keep the server on the same cores (&lt;code&gt;taskset&lt;/code&gt; on Linux). Otherwise you’re measuring the scheduler and thermal noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warmup phase:&lt;/strong&gt; measure p50/p95 after a fixed warmup window. JIT and caches are real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same TLS strategy:&lt;/strong&gt; either terminate TLS at a reverse proxy for both, or do it in-process for both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure behavior:&lt;/strong&gt; if the client can out-send the server, pick a consistent policy (drop, buffer, or slow) and document it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node gives you good primitives for this kind of instrumentation via &lt;code&gt;perf_hooks&lt;/code&gt; and memory stats via &lt;code&gt;process.memoryUsage()&lt;/code&gt; (see the official &lt;a href="https://nodejs.org/en/learn" rel="noopener noreferrer"&gt;Node.js contributors&lt;/a&gt;). Bun’s runtime and server APIs are built around &lt;code&gt;Bun.serve&lt;/code&gt; with integrated WebSocket upgrade handling, as documented by the &lt;a href="https://bun.sh/docs" rel="noopener noreferrer"&gt;Bun team&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want an external “sanity check” for throughput positioning, TechEmpower is useful. Just don’t pretend it’s your workload. Framework choice, DB access, and JSON behavior dominate. The &lt;a href="https://www.techempower.com/benchmarks/" rel="noopener noreferrer"&gt;TechEmpower&lt;/a&gt; suite is famous because it’s broad, not because it matches your app.&lt;/p&gt;

&lt;p&gt;Here’s a current-year video that shows common benchmark patterns (and some pitfalls) in the wild. Watch it for context, not as a substitute for reproducibility:&lt;/p&gt;

&lt;p&gt;[YOUTUBE:_iEaaNIjg7U|Deno vs. Node.js vs. Bun Performance &amp;amp; Comparison (2026)]&lt;/p&gt;

&lt;h3&gt;
  
  
  A small but important reproducibility detail
&lt;/h3&gt;

&lt;p&gt;Cold start numbers are meaningless unless you say what you counted.&lt;/p&gt;

&lt;p&gt;I recommend publishing two cold-start measurements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold start (image already present):&lt;/strong&gt; excludes image pull time. Measures runtime + init + bind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start (image pull included):&lt;/strong&gt; includes pull. Measures your registry/network reality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Kubernetes, the first one influences HPA reaction time. The second one matters during &lt;em&gt;cluster recovery&lt;/em&gt;, node churn, and “the registry is having a day.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: what typically wins on cold start, memory, and WebSockets
&lt;/h2&gt;

&lt;p&gt;I’m going to be blunt. I don’t trust your absolute numbers unless you ran them on your own infra. But I do trust directional differences when the harness is fair and the workload isn’t contrived.&lt;/p&gt;

&lt;p&gt;So here’s the decision table I’d use for 2026, based on how these runtimes are built and what they optimize for.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric (real app lens)&lt;/th&gt;
&lt;th&gt;Bun (JavaScriptCore + &lt;code&gt;Bun.serve&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Node.js (V8 + ecosystem)&lt;/th&gt;
&lt;th&gt;What I’d pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start p50/p95 (container/serverless)&lt;/td&gt;
&lt;td&gt;Usually lower startup overhead. Single-binary story helps.&lt;/td&gt;
&lt;td&gt;Improved over time, but heavier baseline, more moving parts.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bun&lt;/strong&gt; for spiky autoscale services.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle memory (RSS)&lt;/td&gt;
&lt;td&gt;Often lower RSS for simple services.&lt;/td&gt;
&lt;td&gt;Often higher RSS baseline, depends on flags and loaded modules.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bun&lt;/strong&gt; if you’re binpacking.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory under load (RSS + external)&lt;/td&gt;
&lt;td&gt;Can look great until payloads/alloc patterns change. Watch cliffs.&lt;/td&gt;
&lt;td&gt;Tooling to analyze memory is excellent (&lt;code&gt;--inspect&lt;/code&gt;, heap snapshots).&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Node&lt;/strong&gt; if you’re chasing leaks weekly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket echo throughput&lt;/td&gt;
&lt;td&gt;Strong built-in primitives for upgrade + WS handling.&lt;/td&gt;
&lt;td&gt;Depends heavily on library (&lt;code&gt;ws&lt;/code&gt; vs uWebSockets) and tuning.&lt;/td&gt;
&lt;td&gt;Tie. Choose based on your WS library comfort.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket broadcast fan-out&lt;/td&gt;
&lt;td&gt;Can be very good if your server loop is tight and allocations are low.&lt;/td&gt;
&lt;td&gt;You can win, but you’ll work for it. Backpressure patterns matter.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bun&lt;/strong&gt; for greenfield real-time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging/profiling maturity&lt;/td&gt;
&lt;td&gt;Improving, but fewer battle-tested workflows.&lt;/td&gt;
&lt;td&gt;Best-in-class ecosystem and “how to debug this at 3 a.m.” docs.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Node&lt;/strong&gt; for org-scale services.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the practical version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start: what to measure and what it means
&lt;/h3&gt;

&lt;p&gt;Cold start is p95 user experience wearing an ops hat.&lt;/p&gt;

&lt;p&gt;In 2026, scale events happen because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traffic spikes&lt;/li&gt;
&lt;li&gt;deployments roll&lt;/li&gt;
&lt;li&gt;a node gets recycled&lt;/li&gt;
&lt;li&gt;HPA decides your CPU target is being violated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your runtime takes 300 ms longer to become ready, you rarely just “lose 300 ms.” You get queuing behind readiness. You get thundering-herd retries. You get that slightly panicky Slack thread where everyone blames the database because that’s the only graph they trust.&lt;/p&gt;

&lt;p&gt;A good harness should report at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;p50 and p95 cold start&lt;/strong&gt; (in milliseconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;time to first successful request&lt;/strong&gt; after bind&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;time to steady latency&lt;/strong&gt; (after warmup), because first request is often a liar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And publish your environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;instance type / CPU model&lt;/li&gt;
&lt;li&gt;OS and kernel version&lt;/li&gt;
&lt;li&gt;Bun version and Node major&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bun markets itself as reducing startup time and memory via JavaScriptCore and an integrated toolchain. That matches the bet it’s making. Keep the runtime tight, make the default server path fast, ship the thing as a single executable when possible (&lt;a href="https://bun.sh/docs" rel="noopener noreferrer"&gt;Bun team&lt;/a&gt;). Node is still competitive. It’s just optimized for a different constraint set: stability, compatibility, and an ecosystem that’s basically its own economy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory: RSS vs heapUsed vs external (and why you should care)
&lt;/h3&gt;

&lt;p&gt;Most engineers quote &lt;code&gt;heapUsed&lt;/code&gt; because it’s easy to print.&lt;/p&gt;

&lt;p&gt;It’s also the wrong number to optimize if your real problem is “why can’t we fit more replicas on this node?”&lt;/p&gt;

&lt;p&gt;What matters in containers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RSS&lt;/strong&gt;: what the kernel thinks you’re using. This is what triggers OOM and eviction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;heapUsed&lt;/strong&gt;: what V8 thinks is on-heap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;external&lt;/strong&gt;: buffers, native allocations, and “stuff not counted in heapUsed.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node exposes these quickly with &lt;code&gt;process.memoryUsage()&lt;/code&gt; and has genuinely good docs on memory profiling and diagnostics (&lt;a href="https://nodejs.org/en/learn" rel="noopener noreferrer"&gt;Node.js contributors&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Bun’s memory story can look fantastic for simple HTTP services. The question I care about is: does it stay well-behaved when the workload is annoying?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does RSS climb after 10 minutes of WebSocket churn?&lt;/li&gt;
&lt;li&gt;Does it flatten after GC cycles?&lt;/li&gt;
&lt;li&gt;Does it drop after load stops?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run WebSockets at scale, you’re usually holding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;socket buffers&lt;/li&gt;
&lt;li&gt;per-connection state&lt;/li&gt;
&lt;li&gt;room membership maps&lt;/li&gt;
&lt;li&gt;outbound queues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those allocations are where runtimes show their personality.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSockets: echo is easy. broadcast is the benchmark.
&lt;/h3&gt;

&lt;p&gt;Almost every “WebSocket benchmark” online is an echo test.&lt;/p&gt;

&lt;p&gt;Echo tests are fine for validating overhead. They’re useless for sizing a chat system, a presence system, or anything where one message fans out to a bunch of sockets.&lt;/p&gt;

&lt;p&gt;What you want is a broadcast fan-out test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;N clients connected&lt;/li&gt;
&lt;li&gt;clients join rooms&lt;/li&gt;
&lt;li&gt;server broadcasts a message to a room&lt;/li&gt;
&lt;li&gt;measure &lt;strong&gt;messages delivered/sec&lt;/strong&gt; and &lt;strong&gt;p95 delivery latency&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two patterns create very different failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connection churn:&lt;/strong&gt; mobile networks, LB rebalancing, clients that reconnect on deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-lived steady connections:&lt;/strong&gt; dashboards, chat, multiplayer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bun’s &lt;code&gt;Bun.serve&lt;/code&gt; combines HTTP and WS upgrades in one primitive, which reduces glue code. Glue code is where you accidentally allocate, accidentally buffer, and accidentally never free (&lt;a href="https://bun.sh/docs" rel="noopener noreferrer"&gt;Bun team&lt;/a&gt;). In Node, you can absolutely build a great WS server. You just have more knobs to get wrong: library choice, upgrade handling, ping/pong strategy, backpressure policy.&lt;/p&gt;

&lt;p&gt;If you want a sanity check: uWebSockets.js is often the “fast path” people reach for in Node, while &lt;code&gt;ws&lt;/code&gt; is the default ergonomic choice. That gap alone can dwarf the Bun vs Node delta, which is why “runtime X is faster” is such a lazy take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment implications: turning benchmarks into dollars and incidents
&lt;/h2&gt;

&lt;p&gt;This is where I stop caring about benchmark culture and start caring about consequences.&lt;/p&gt;

&lt;p&gt;Benchmarks are only useful if you can turn them into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pods per node&lt;/strong&gt; (binpacking)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;autoscale reaction time&lt;/strong&gt; (cold start)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tail latency&lt;/strong&gt; under connection fan-out&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Container density math (the boring answer that saves money)
&lt;/h3&gt;

&lt;p&gt;Let’s say you run a 16 GiB node and reserve 2 GiB for system overhead. You have &lt;strong&gt;14 GiB&lt;/strong&gt; for workloads.&lt;/p&gt;

&lt;p&gt;If your runtime idles at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200 MiB RSS&lt;/strong&gt; per replica, you can fit ~70 replicas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;350 MiB RSS&lt;/strong&gt; per replica, you can fit ~40 replicas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a &lt;strong&gt;1.75×&lt;/strong&gt; difference in density.&lt;/p&gt;

&lt;p&gt;Then reality shows up. Sidecars. Agents. “Temporary” debug flags that never get removed. Suddenly your cluster upgrade budget is a negotiation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Autoscaling and cold start: p95 is the tax you pay during deploys
&lt;/h3&gt;

&lt;p&gt;If your p95 cold start is even &lt;strong&gt;500 ms&lt;/strong&gt; worse, you don’t just lose 500 ms.&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;longer readiness&lt;/li&gt;
&lt;li&gt;bigger queues&lt;/li&gt;
&lt;li&gt;more time in overload&lt;/li&gt;
&lt;li&gt;more retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s how you get cascading latency incidents that present as “the database is slow” but started as “pods took too long to come up.”&lt;/p&gt;

&lt;p&gt;If you care about this, you’ll probably also care about system-level observability. I’ve written about how to get high-signal data without drowning in telemetry in &lt;a href="https://dev.to/blog/ebpf-observability-sidecars-kubernetes"&gt;7-Step Plan: eBPF Observability Without Sidecars on Kubernetes&lt;/a&gt; and how to avoid p99 cliffs in &lt;a href="https://dev.to/blog/transparent-huge-pages-postgres-performance"&gt;Transparent Huge Pages + Postgres: Stop P99 Latency Cliffs [2026]&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket deployments: load balancers, sticky routing, and failure domains
&lt;/h3&gt;

&lt;p&gt;The operational trap with WebSockets is that the bottleneck moves.&lt;/p&gt;

&lt;p&gt;You’ll run into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LB connection limits&lt;/strong&gt; (count your max connections per target)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sticky sessions&lt;/strong&gt; or consistent hashing (rooms need locality)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;broadcast topology&lt;/strong&gt; (single node vs pub/sub)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re doing fan-out, you’re doing distributed systems. Pretending otherwise is how you end up debugging ghost disconnects at 2 a.m.&lt;/p&gt;

&lt;p&gt;If you’re running this on Kubernetes, the boring rules still apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set realistic &lt;code&gt;readinessProbe&lt;/code&gt; and &lt;code&gt;startupProbe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tune &lt;code&gt;net.core.somaxconn&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;raise &lt;code&gt;ulimit -n&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;measure connection churn explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When I’d choose Bun vs when I’d stick with Node
&lt;/h2&gt;

&lt;p&gt;Here’s my stance for 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bun is a safe win when…
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You’re building a &lt;strong&gt;greenfield&lt;/strong&gt; HTTP + WebSocket service.&lt;/li&gt;
&lt;li&gt;You care about &lt;strong&gt;cold start and memory footprint&lt;/strong&gt; more than obscure ecosystem dependencies.&lt;/li&gt;
&lt;li&gt;You want a simpler runtime story (single toolchain, fewer moving parts).&lt;/li&gt;
&lt;li&gt;Your WebSocket workload is heavy on &lt;strong&gt;concurrent connections&lt;/strong&gt; and you can keep the app logic lean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stick with Node when…
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You rely on a mature ecosystem: frameworks, observability agents, native modules.&lt;/li&gt;
&lt;li&gt;You need predictable release and support expectations (LTS cadence is a real organizational feature).&lt;/li&gt;
&lt;li&gt;Your team’s debugging muscle memory is Node-shaped, and you don’t want to pay the transition tax.&lt;/li&gt;
&lt;li&gt;You ship anything involving native addons or complex deployment constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of those cases where the boring answer is actually the right one. Most teams should not migrate a stable Node system just because Bun wins a benchmark. You migrate when the infra math is screaming and you’ve run the harness enough times to trust it.&lt;/p&gt;

&lt;p&gt;If you’re already deep in JavaScript/TypeScript architecture decisions, you might also like &lt;a href="https://dev.to/blog/typescript-vs-javascript-2026"&gt;TypeScript vs JavaScript 2026: Type Safety Finally Worth the Cost?&lt;/a&gt; and &lt;a href="https://dev.to/blog/hono-vs-express-2026"&gt;Hono vs Express in 2026: Which API Framework Actually Wins?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScriptCore vs V8: why runtime architecture shows up as infra behavior
&lt;/h2&gt;

&lt;p&gt;The engine matters. Just not in the simplistic “JSC fast, V8 slow” way people argue about online.&lt;/p&gt;

&lt;p&gt;Bun is powered by &lt;strong&gt;JavaScriptCore&lt;/strong&gt; and is built as an integrated toolkit. Node is powered by &lt;strong&gt;V8&lt;/strong&gt; and sits at the center of a massive ecosystem.&lt;/p&gt;

&lt;p&gt;In practice, the engine difference shows up when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you care about startup behavior and baseline overhead&lt;/li&gt;
&lt;li&gt;your app allocates aggressively (buffers, JSON parsing, WS fan-out)&lt;/li&gt;
&lt;li&gt;you need profiling and diagnostics at depth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node can go very deep on diagnostics and memory profiling. Bun’s bet is that a tighter runtime plus a tight server primitive keeps the hot path simpler and cheaper.&lt;/p&gt;

&lt;p&gt;If you want a mental model: engines influence the &lt;em&gt;shape&lt;/em&gt; of performance cliffs. Your harness exists to find those cliffs before your users do.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quick note on microbenchmarks
&lt;/h3&gt;

&lt;p&gt;Microbenchmarks are still useful. I treat them like unit tests for performance regressions, not like decision-makers.&lt;/p&gt;

&lt;p&gt;TechEmpower is a good reminder that frameworks and workload mixes dominate outcomes. A runtime that wins “plaintext” can lose “JSON + DB,” and both can lose “WebSockets + fan-out.” Use &lt;a href="https://www.techempower.com/benchmarks/" rel="noopener noreferrer"&gt;TechEmpower&lt;/a&gt; as context, not as a verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  My 2026 prediction: Bun will win more greenfield services, but Node will remain the default
&lt;/h2&gt;

&lt;p&gt;Bun is doing what ambitious runtimes should do. It’s compressing the stack and making “fast by default” real for a class of services.&lt;/p&gt;

&lt;p&gt;Node is doing what mature platforms should do. It’s staying dependable while still improving.&lt;/p&gt;

&lt;p&gt;My prediction: by the end of 2026, Bun will be a common choice for &lt;strong&gt;real-time, connection-heavy services&lt;/strong&gt; where cold start and RSS are first-order costs. Node will stay the default for everything that leans on the ecosystem and long-term operability.&lt;/p&gt;

&lt;p&gt;If you want to pick intelligently, stop arguing about ideology. Build a harness that matches your workload, publish the numbers, and let your infrastructure tell you the truth.&lt;/p&gt;

&lt;p&gt;Internal reading trail if you’re in “production mindset” mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/docker-compose-kubernetes-ai-ml-2026"&gt;Docker Compose vs Kubernetes for AI/ML [2026]: Use Which?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/debug-http3-quic-production"&gt;Debug HTTP/3 QUIC in Production: 8-Step Playbook [2026]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/llm-latency-benchmark-methodology"&gt;LLM Latency Benchmark Methodology: Streaming UX Metrics [2026]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/blog/reproducible-terminal-dev-environment"&gt;Reproducible Terminal Dev Environment: direnv + mise [2026]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you’re building systems that include &lt;strong&gt;AI agents&lt;/strong&gt; in the backend, you’ll care even more about tail latency and connection stability. Start here: &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also, if your team is trying to ship fast with &lt;strong&gt;Claude Code&lt;/strong&gt; or other agentic coding tools, treat runtime changes as part of your risk surface, not just your performance plan: &lt;a href="https://dev.to/blog/claude-code-security-2026"&gt;Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, don’t forget that “fast runtime” doesn’t save you from app-layer attacks. If you run WebSockets with user-generated content, you’re eventually going to deal with input that tries to control the system. The same mindset behind &lt;strong&gt;prompt injection&lt;/strong&gt; applies to any system that executes actions based on untrusted text: &lt;a href="https://dev.to/blog/prompt-injection-2026-owasp-llm-vulnerability"&gt;prompt injection&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/bun-vs-node-performance-2026?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=bun-vs-node-performance-2026" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bunjs</category>
      <category>node</category>
      <category>benchmarks</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Pick LLM Application Observability Metrics [2026]</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Sat, 15 Aug 2026 12:41:58 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/how-to-pick-llm-application-observability-metrics-2026-1eeb</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/how-to-pick-llm-application-observability-metrics-2026-1eeb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/llm-observability-metrics" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Last year I watched a team burn three days on an “LLM incident” that had nothing to do with the model. p99 doubled, costs spiked, quality dipped. The only thing they had was token counts and a big pile of prompt/response logs. So the debugging session turned into archaeology.&lt;/p&gt;

&lt;p&gt;You’ll finish this in about 45 minutes with a working “minimum viable” LLM observability setup: a trace model (LLM + retrieval + tool calls + retries), a short metrics list, and a privacy-safe logging policy you can ship without storing raw prompts.&lt;/p&gt;

&lt;p&gt;If you’re only collecting token counts and prompt/response logs, you don’t have &lt;strong&gt;LLM application observability metrics&lt;/strong&gt;. You have an expensive transcript.&lt;/p&gt;

&lt;p&gt;This post is about the signals that explain the stuff that actually breaks in production: tool calls that time out, retrieval that quietly returns garbage, caches that “help” by serving wrong answers faster, and safety filters that suddenly start refusing half your traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is LLM application observability?
&lt;/h2&gt;

&lt;p&gt;LLM application observability is instrumenting an LLM-powered system with traces, metrics, and logs so you can explain latency, cost, failures, and answer-quality changes across the full request path. That means the model call, retrieval, tool/function calls, retries, caching, and safety.&lt;/p&gt;

&lt;p&gt;The important part is “full request path”. The model is rarely the product.&lt;/p&gt;

&lt;p&gt;I learned this the hard way building the Walmart conversational commerce chatbot at Firework (Zealsight). We handled &lt;strong&gt;millions of queries daily&lt;/strong&gt; with &lt;strong&gt;sub-second responses&lt;/strong&gt;, and the real wins came from tracing the pipeline (retrieval + streaming + orchestration), not staring at token logs. At that scale, “the LLM was slow” is basically never a root cause. It’s a symptom you slap on anything you can’t see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimum signals checklist (copy this into your dashboard)
&lt;/h2&gt;

&lt;p&gt;This is the smallest set of signals I’d accept before calling an LLM feature “production-ready”. Keep it tight. You can always add the fancy stuff later. What you can’t do is debug p99 with vibes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;How to capture&lt;/th&gt;
&lt;th&gt;Suggested metric names&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end request latency (p50/p95/p99)&lt;/td&gt;
&lt;td&gt;Users feel p95. Incidents live in p99.&lt;/td&gt;
&lt;td&gt;Server span around the whole request.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;llm_app.request.latency_ms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM call latency + TTFT&lt;/td&gt;
&lt;td&gt;Separates model slowness from your pipeline slowness.&lt;/td&gt;
&lt;td&gt;Span per model call; record TTFT as event/exemplar.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;llm.call.latency_ms&lt;/code&gt;, &lt;code&gt;llm.call.ttft_ms&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool/function call success + latency&lt;/td&gt;
&lt;td&gt;Tools fail more than models. Also where most hidden retries happen.&lt;/td&gt;
&lt;td&gt;Span per tool call with status + attempts.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool.call.success_rate&lt;/code&gt;, &lt;code&gt;tool.call.latency_ms&lt;/code&gt;, &lt;code&gt;tool.call.retries_total&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema/validation mismatch rate&lt;/td&gt;
&lt;td&gt;“It returned JSON” is not correctness.&lt;/td&gt;
&lt;td&gt;Validate tool args and structured outputs.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tool.call.schema_mismatch_rate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval latency breakdown&lt;/td&gt;
&lt;td&gt;RAG regressions often start as latency regressions.&lt;/td&gt;
&lt;td&gt;Separate spans: embed → search → rerank.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rag.embed.latency_ms&lt;/code&gt;, &lt;code&gt;rag.search.latency_ms&lt;/code&gt;, &lt;code&gt;rag.rerank.latency_ms&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty retrieval rate&lt;/td&gt;
&lt;td&gt;“No docs found” should be loud, not silent.&lt;/td&gt;
&lt;td&gt;Count retrievals with 0 usable chunks.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rag.empty_result_rate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top-k actually used&lt;/td&gt;
&lt;td&gt;If you ask for k=20 but use 3, you’re paying for nothing.&lt;/td&gt;
&lt;td&gt;Log &lt;code&gt;k_requested&lt;/code&gt;, &lt;code&gt;k_returned&lt;/code&gt;, &lt;code&gt;k_used&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rag.k_used&lt;/code&gt; (histogram)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chunk duplication/overlap&lt;/td&gt;
&lt;td&gt;High overlap inflates tokens and reduces diversity.&lt;/td&gt;
&lt;td&gt;Hash chunk IDs; compute overlap ratio.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rag.chunk_overlap_ratio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit rate (prompt/embedding/semantic)&lt;/td&gt;
&lt;td&gt;Cache changes cost and latency by multiples, and can regress correctness.&lt;/td&gt;
&lt;td&gt;Count hits/misses by cache tier + version.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cache.prompt.hit_rate&lt;/code&gt;, &lt;code&gt;cache.embedding.hit_rate&lt;/code&gt;, &lt;code&gt;cache.semantic.hit_rate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refusal / safe-completion rate&lt;/td&gt;
&lt;td&gt;Safety behavior changes over time and by cohort.&lt;/td&gt;
&lt;td&gt;Classify outcome category without storing raw prompt.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;safety.refusal_rate&lt;/code&gt;, &lt;code&gt;safety.safe_completion_rate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII redaction rate&lt;/td&gt;
&lt;td&gt;Proves your privacy controls are working.&lt;/td&gt;
&lt;td&gt;Redact before export; count redactions.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;privacy.redaction_events_total&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sampling + retention compliance&lt;/td&gt;
&lt;td&gt;Prevents “observability becomes a data leak”.&lt;/td&gt;
&lt;td&gt;Enforce TTL + sampled logging.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;telemetry.sample_rate&lt;/code&gt;, &lt;code&gt;telemetry.retention_days&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on numbers: graph p50/p95/p99 for anything latency-related. Keep at least &lt;strong&gt;7 days&lt;/strong&gt; of metrics at full fidelity. Logs can be sampled way harder (often &lt;strong&gt;0.1%–1%&lt;/strong&gt; for high-volume apps) if you have traces and exemplars.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to structure LLM telemetry as traces, metrics, and events (so your APM works)
&lt;/h2&gt;

&lt;p&gt;Most teams accidentally create a new observability universe for LLMs. They ship a vendor dashboard, add a giant JSON log blob, sprinkle some notebooks around. Six months later, nobody trusts any of it, and on-call falls back to… reading prompts.&lt;/p&gt;

&lt;p&gt;I’m blunt about this: &lt;strong&gt;force LLM telemetry into standard primitives&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; answer: “Where did the time go?”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; answer: “Is it getting worse over time, and for whom?”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs/events&lt;/strong&gt; answer: “What exactly happened on this one weird request?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already run OpenTelemetry (OTel), you’re most of the way there. I wrote a deeper implementation guide in &lt;a href="https://dev.to/blog/opentelemetry-ai-agents-instrumentation"&gt;OpenTelemetry Instrumentation for AI Agents [2026]: Ship It&lt;/a&gt;. I’m not going to duplicate the schema here. What you need from this post is a trace shape you can standardize on and the minimum set of attributes that won’t get you hauled into a privacy review.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trace shape I use for LLM apps
&lt;/h3&gt;

&lt;p&gt;You want one trace per user request, with spans that map to real pipeline boundaries. Not “AI stuff happened here”. Real boundaries.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;http.request&lt;/code&gt;&lt;/strong&gt; (root span)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm.plan&lt;/code&gt;&lt;/strong&gt; (optional, if you do multi-step / agentic planning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rag.retrieve&lt;/code&gt;&lt;/strong&gt; (optional, if you do &lt;a href="https://dev.to/glossary/rag"&gt;RAG&lt;/a&gt;)

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rag.embed_query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rag.vector_search&lt;/code&gt; (or hybrid search)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rag.rerank&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool.call.&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/strong&gt; (0..N)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm.generate&lt;/code&gt;&lt;/strong&gt; (the final response)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response.stream&lt;/code&gt;&lt;/strong&gt; (optional; track streaming UX separately)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule I won’t compromise on: &lt;strong&gt;every external dependency gets its own span&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Payments API. Span.&lt;/p&gt;

&lt;p&gt;Postgres lookup. Span.&lt;/p&gt;

&lt;p&gt;Vector DB call. Span.&lt;/p&gt;

&lt;p&gt;If you don’t do this, your p99 chart is just a mystery novel where every suspect has an alibi.&lt;/p&gt;

&lt;p&gt;Treat the LLM like a microservice. Because that’s what it is in your architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimum span attributes that don’t leak data
&lt;/h3&gt;

&lt;p&gt;I try to avoid raw prompt logging by default. Instead, I put join keys on spans so I can correlate behavior across versions and cohorts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;request_id&lt;/code&gt; (server-generated)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_cohort&lt;/code&gt; (coarse: &lt;code&gt;free&lt;/code&gt;, &lt;code&gt;pro&lt;/code&gt;, &lt;code&gt;internal&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model.name&lt;/code&gt; + &lt;code&gt;model.version&lt;/code&gt; (or provider + deployment)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompt_template_id&lt;/code&gt; + &lt;code&gt;prompt_version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rag.corpus_id&lt;/code&gt; + &lt;code&gt;rag.index_version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool.name&lt;/code&gt; + &lt;code&gt;tool.version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache.tier&lt;/code&gt; + &lt;code&gt;cache.key_version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;safety.outcome&lt;/code&gt; (enum)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those version fields matter more than people think. Half of the “LLM randomness” incidents I see are actually “we changed an index / prompt / tool schema and forgot to correlate it”. Then everyone argues about temperature settings like that’s the problem.&lt;/p&gt;

&lt;p&gt;If you want a concrete example of versioning and pipeline identity, my &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;AI Engineering Evals: Regression Gates for Prompts, Tools, RAG [2026]&lt;/a&gt; post shows how I tie observability back to eval gates.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to trace and measure tool/function calls (latency, errors, retries, schema mismatch)
&lt;/h2&gt;

&lt;p&gt;Tool calls are where demos go to die.&lt;/p&gt;

&lt;p&gt;Your LLM can be perfect and your app still fails because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tool timed out at &lt;strong&gt;2,000 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the LLM produced invalid args &lt;strong&gt;3 times&lt;/strong&gt; and you retried&lt;/li&gt;
&lt;li&gt;the upstream API returned &lt;strong&gt;429&lt;/strong&gt; and you backoff-looped&lt;/li&gt;
&lt;li&gt;the tool “succeeded” but returned an empty payload (a silent failure)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I measure for each tool
&lt;/h3&gt;

&lt;p&gt;For each tool/function, I want five things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Success rate&lt;/strong&gt;: &lt;code&gt;success / attempts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: p50/p95/p99 of the tool span&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry count distribution&lt;/strong&gt; (histogram, not a single average)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error categories&lt;/strong&gt; (timeouts vs 4xx vs 5xx vs validation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema mismatch rate&lt;/strong&gt; (LLM produced args that didn’t validate)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do only one thing: &lt;strong&gt;break retries out as their own spans&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Retries are the sneakiest source of “why did p99 double?” because the user only sees the final response. You quietly did 2–4 extra round trips and congratulated yourself for “recovering”.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to attribute downstream failures to the right span
&lt;/h3&gt;

&lt;p&gt;I see a lot of traces where everything is marked “OK” except the root span. That’s useless.&lt;/p&gt;

&lt;p&gt;A simple rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the tool request failed (timeout / non-2xx), mark the tool span as error.&lt;/li&gt;
&lt;li&gt;If the tool returned data but you rejected it (schema/validation), mark the tool span as error.&lt;/li&gt;
&lt;li&gt;If the tool succeeded but the LLM refused to use it, do not mark the tool span as error. Mark a &lt;strong&gt;decision event&lt;/strong&gt; on the &lt;code&gt;llm.plan&lt;/code&gt; or &lt;code&gt;llm.generate&lt;/code&gt; span.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction is how you stop blaming infra for product behavior.&lt;/p&gt;

&lt;p&gt;If you’re building &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt; or doing &lt;a href="https://dev.to/glossary/agent-orchestration"&gt;agent orchestration&lt;/a&gt;, tool-call observability is your real “agent reliability” layer. The model is the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache metrics that actually matter (and how to detect cache-caused regressions)
&lt;/h2&gt;

&lt;p&gt;Caches are a first-class dependency in LLM apps. Pretending they’re just an implementation detail is how you get blindsided.&lt;/p&gt;

&lt;p&gt;You usually have at least two of these. Most teams end up with all three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt cache&lt;/strong&gt; (exact match, or templated)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding cache&lt;/strong&gt; (query embedding reuse)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic cache&lt;/strong&gt; (approximate match, “close enough”)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you don’t instrument caches, you’ll misread both cost and quality.&lt;/p&gt;

&lt;p&gt;A semantic cache can cut cost by &lt;strong&gt;30%–60%&lt;/strong&gt; in high-repeat workloads. It can also destroy correctness when the query is similar but not identical. Faster wrong answers are still wrong.&lt;/p&gt;

&lt;p&gt;I go deeper on cost levers in &lt;a href="https://dev.to/blog/reduce-llm-api-costs-production"&gt;Reduce LLM API Costs 60%: 6 Techniques [2026]&lt;/a&gt; and per-task accounting in &lt;a href="https://dev.to/blog/agent-per-task-cost-calculation"&gt;Agent Per-Task Cost Calculation [2026]: Retries, Tools, Caching&lt;/a&gt;. Here I’m focusing on what to observe so you can catch regressions before support tickets do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimum cache metrics per tier
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cache.&amp;lt;tier&amp;gt;.hit_rate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache.&amp;lt;tier&amp;gt;.lookup_latency_ms&lt;/code&gt; (p95 matters)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache.&amp;lt;tier&amp;gt;.stale_served_rate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache.&amp;lt;tier&amp;gt;.bypass_rate&lt;/code&gt; (how often your app skips cache)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache.&amp;lt;tier&amp;gt;.key_version_mismatch_rate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is your regression smoke alarm.&lt;/p&gt;

&lt;p&gt;If you change prompt templates or tool schemas and don’t bump cache key versions, your cache becomes a correctness bug factory. You’ll see “great latency, bad answers” and the Slack thread will immediately blame the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detecting cache-caused quality regressions without storing prompts
&lt;/h3&gt;

&lt;p&gt;You can detect most cache regressions with three join keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prompt_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache.key_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eval_outcome_bucket&lt;/code&gt; (or a proxy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run continuous evals, correlate “bad outcome” with cache hit/miss.&lt;/p&gt;

&lt;p&gt;If you don’t have full evals yet, use proxies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user rephrase rate within &lt;strong&gt;30 seconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;user click-through on citations (for RAG)&lt;/li&gt;
&lt;li&gt;“thumbs down” rate&lt;/li&gt;
&lt;li&gt;session abandonment after answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They’re imperfect. They beat guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG observability metrics for production (and how to debug answer quality drops)
&lt;/h2&gt;

&lt;p&gt;Retrieval-Augmented Generation (RAG) is where most “LLM apps” become real products. It’s also where people ship the least observability.&lt;/p&gt;

&lt;p&gt;RAG failures are usually boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retrieval returned nothing&lt;/li&gt;
&lt;li&gt;retrieval returned duplicates&lt;/li&gt;
&lt;li&gt;retrieval returned irrelevant chunks&lt;/li&gt;
&lt;li&gt;reranker got slower and you silently lowered k&lt;/li&gt;
&lt;li&gt;index got rebuilt and semantics shifted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the Walmart chatbot, one lesson kept repeating: &lt;strong&gt;retrieval quality dominated answer quality at scale&lt;/strong&gt;. Model upgrades were incremental. Retrieval regressions were catastrophic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimum RAG observability metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; by stage: embedding, search, rerank&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty-result rate&lt;/strong&gt; (&lt;code&gt;k_returned = 0&lt;/code&gt; or &lt;code&gt;usable_chunks = 0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-k distribution&lt;/strong&gt;: requested vs returned vs used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk overlap ratio&lt;/strong&gt;: duplicates / near-duplicates in the final context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citation coverage&lt;/strong&gt;: how often your answer cites at least 1 retrieved chunk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context token budget&lt;/strong&gt;: tokens spent on retrieved context vs generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want my take on why “just increase the context window” is a trap, see &lt;a href="https://dev.to/blog/rag-context-window-limitations"&gt;RAG Context Window Limits: Why Bigger Is Not Better [2026]&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging playbook: “answer quality dropped”
&lt;/h3&gt;

&lt;p&gt;When someone tells me “quality dropped”, I don’t start by debating model upgrades. I run this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check &lt;code&gt;rag.empty_result_rate&lt;/code&gt; for a spike. If it goes from &lt;strong&gt;0.5%&lt;/strong&gt; to &lt;strong&gt;5%&lt;/strong&gt;, that’s your incident.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;rag.search.latency_ms p99&lt;/code&gt;. If p99 jumped, some callers will time out and fall back.&lt;/li&gt;
&lt;li&gt;Compare &lt;code&gt;rag.index_version&lt;/code&gt;. If the index rebuilt, correlate the quality change with the version boundary.&lt;/li&gt;
&lt;li&gt;Look at &lt;code&gt;rag.chunk_overlap_ratio&lt;/code&gt;. If overlap jumped, your chunking pipeline probably changed.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;rag.k_used&lt;/code&gt;. If you silently started using fewer chunks, you probably introduced a budget clamp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is exactly why you track index identity in spans. Without it, you can’t correlate, so you can’t fix.&lt;/p&gt;

&lt;p&gt;If you’re doing &lt;a href="https://dev.to/glossary/retrieval-augmented-generation"&gt;retrieval-augmented generation&lt;/a&gt; seriously, you’ll eventually want offline evals and replay harnesses. My &lt;a href="https://dev.to/blog/agent-evaluation-harness-replay"&gt;Agent Evaluation Harness [2026]: Replay, Rubrics, CI Gates&lt;/a&gt; post is the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refusal rate and safety outcomes (without storing sensitive prompts)
&lt;/h2&gt;

&lt;p&gt;If your refusal rate is “unknown”, you don’t have a production system. You have a liability.&lt;/p&gt;

&lt;p&gt;Track these as metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;safety.refusal_rate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;safety.safe_completion_rate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;safety.policy_trigger_rate&lt;/code&gt; (by category)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then correlate them with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model.version&lt;/code&gt; (providers change behavior)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompt_version&lt;/code&gt; (you change behavior)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool.name&lt;/code&gt; (tools can trip policy boundaries)&lt;/li&gt;
&lt;li&gt;user cohort&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A refusal spike can come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an upstream model update&lt;/li&gt;
&lt;li&gt;an overly aggressive content filter&lt;/li&gt;
&lt;li&gt;a new tool that surfaces sensitive data&lt;/li&gt;
&lt;li&gt;a &lt;a href="https://dev.to/blog/prompt-injection-2026-owasp-llm-vulnerability"&gt;prompt injection&lt;/a&gt; chain that tries to jailbreak the system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re not threat-modeling this, start here: &lt;a href="https://dev.to/blog/ai-security-complete-guide"&gt;AI security&lt;/a&gt;. I also keep a checklist in &lt;a href="https://dev.to/glossary/ai-security"&gt;AI security&lt;/a&gt; that matches how I think about “safe by default” telemetry.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to classify outcomes without logging prompts
&lt;/h3&gt;

&lt;p&gt;Use an enum outcome plus a coarse reason code.&lt;/p&gt;

&lt;p&gt;Example outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ok&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refused&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;partial&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;safe_completion&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool_blocked&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reason codes can be your taxonomy. Keep it stable and versioned. If you change your categories every month, you’re destroying your own trend lines.&lt;/p&gt;

&lt;p&gt;The point is to measure behavior without hoarding content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy-safe LLM logging: what to store, what to hash, sampling, retention
&lt;/h2&gt;

&lt;p&gt;“Just log the prompts” is the GenAI equivalent of “just run it as root”. It works right up until the day it ruins your week.&lt;/p&gt;

&lt;p&gt;I’m not saying “never store prompts”. I’m saying: &lt;strong&gt;make prompt storage an explicit, reviewed exception&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a dedicated privacy playbook in &lt;a href="https://dev.to/blog/data-privacy-rag-redaction-retention"&gt;Data Privacy in RAG Redaction and Retention [2026 Playbook]&lt;/a&gt; and the security angle in &lt;a href="https://dev.to/blog/prevent-sensitive-data-leakage-rag"&gt;Prevent Sensitive Data Leakage in RAG: The 2026 Playbook&lt;/a&gt;. Here’s the minimum policy I’d ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Default: store structure, not content
&lt;/h3&gt;

&lt;p&gt;Store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt template ID + version&lt;/li&gt;
&lt;li&gt;tool names + versions&lt;/li&gt;
&lt;li&gt;retrieval corpus ID + index version&lt;/li&gt;
&lt;li&gt;token counts and cost estimates&lt;/li&gt;
&lt;li&gt;safety outcomes&lt;/li&gt;
&lt;li&gt;hashes of prompts/responses (salted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t store by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw user prompts&lt;/li&gt;
&lt;li&gt;raw model outputs&lt;/li&gt;
&lt;li&gt;raw retrieved chunks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need debugging content, use short-lived secure capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gated behind a feature flag&lt;/li&gt;
&lt;li&gt;enabled for a single &lt;code&gt;request_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;TTL of &lt;strong&gt;24 hours&lt;/strong&gt; (or less)&lt;/li&gt;
&lt;li&gt;access logged and limited to on-call&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Redaction and hashing that’s actually useful
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Redact obvious PII before telemetry export.&lt;/li&gt;
&lt;li&gt;Hash the canonicalized prompt (lowercased, whitespace-normalized) with a rotating salt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Canonicalize because otherwise you can’t correlate duplicates.&lt;/p&gt;

&lt;p&gt;Rotate salts because otherwise your logs become a tracking vector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sampling strategy for high-volume LLM apps
&lt;/h3&gt;

&lt;p&gt;A sane default I’ve used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100% metrics&lt;/strong&gt; (cheap)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10% traces&lt;/strong&gt; for baseline, plus &lt;strong&gt;100% traces on errors&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1% logs&lt;/strong&gt; (structured events), plus “burst” logging for a single incident window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re doing millions of requests/day, logging every prompt is not just risky. It’s expensive.&lt;/p&gt;

&lt;p&gt;If you need to reason about cost tradeoffs, I keep my own datasets on pricing and performance. Based on the benchmark and cost data I maintain at &lt;a href="https://dev.to/llm-prices"&gt;kunalganglani.com/llm-prices&lt;/a&gt; and &lt;a href="https://dev.to/llm-benchmarks"&gt;kunalganglani.com/llm-benchmarks&lt;/a&gt;, the gap between “fast cheap model” and “frontier model” can be multiple times in $/request. Observability that can attribute cost to spans is how you stop arguing in Slack and start making decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation notes: don’t create an observability silo
&lt;/h2&gt;

&lt;p&gt;The goal isn’t “an LLM dashboard”. The goal is: when p99 spikes, your on-call uses the same workflows they use for every other service.&lt;/p&gt;

&lt;p&gt;A few practical moves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Emit tool spans as normal client spans. Your APM already knows how to graph dependency latency.&lt;/li&gt;
&lt;li&gt;Put LLM metadata on spans as attributes, not log blobs.&lt;/li&gt;
&lt;li&gt;Use exemplars to attach “this trace is a p99 outlier” to your histograms.&lt;/li&gt;
&lt;li&gt;Keep a strict boundary between telemetry and product analytics. They can share IDs, not raw content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re running &lt;a href="https://dev.to/glossary/production-ai"&gt;production AI&lt;/a&gt; systems, treat observability changes like API changes. Version them. Review them. Test them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable prediction
&lt;/h2&gt;

&lt;p&gt;Within 12 months, “token logs” will be viewed the way we now view &lt;code&gt;console.log&lt;/code&gt; in a backend service. Fine for a demo. Embarrassing in production.&lt;/p&gt;

&lt;p&gt;If you’re building LLM apps, pick &lt;strong&gt;10–12 signals&lt;/strong&gt;, wire them into traces/metrics/events, and ship a privacy policy with the same seriousness you ship auth.&lt;/p&gt;

&lt;p&gt;Then do the hard part. When your dashboards light up, don’t blame the model. Fix your pipeline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/llm-observability-metrics?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=llm-observability-metrics" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llmops</category>
      <category>observability</category>
      <category>productionai</category>
      <category>opentelemetry</category>
    </item>
    <item>
      <title>7-Step Plan: eBPF Observability Without Sidecars on Kubernetes</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Sat, 15 Aug 2026 00:41:39 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/7-step-plan-ebpf-observability-without-sidecars-on-kubernetes-32n</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/7-step-plan-ebpf-observability-without-sidecars-on-kubernetes-32n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/ebpf-observability-sidecars-kubernetes" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;eBPF observability without sidecars on Kubernetes is a node-level approach where a DaemonSet attaches eBPF programs to the Linux kernel to infer network and syscall-level behavior, instead of injecting an Envoy (or similar) proxy into every pod. I care about this right now for one boring reason: clusters keep getting denser, and the “small” per-pod tax of sidecars turns into real money and real incidents. Also, P99 latency is what users feel. Not your averages. Not your dashboards that look green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sidecarless eBPF gives you cheaper, simpler baseline network visibility, but it does not replace in-process tracing for business-level spans and custom attributes.&lt;/li&gt;
&lt;li&gt;If you remove sidecars, you must re-validate P99 with a canary plan and explicit rollback criteria, or you will ship latency regressions with false confidence.&lt;/li&gt;
&lt;li&gt;Kernel capability is the real platform dependency for eBPF. Managed Kubernetes makes this a procurement and node-image problem, not an “app team” problem.&lt;/li&gt;
&lt;li&gt;Ambient and hybrid patterns (gateway-only proxies, partial mesh, node agents) are usually the right end state, not an ideological “no proxies ever.”&lt;/li&gt;
&lt;li&gt;Cardinality is a budget, not a surprise. Treat labels like an API and enforce limits before the first rollout.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you cannot describe your rollback trigger in one sentence, you are not ready to remove sidecars.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you gain and lose vs service mesh sidecars (Envoy)
&lt;/h2&gt;

&lt;p&gt;Let’s stop pretending this is a moral debate. It’s a trade.&lt;/p&gt;

&lt;p&gt;A service mesh sidecar (usually Envoy) buys you &lt;em&gt;semantic&lt;/em&gt; visibility and control at Layer 7 because it literally terminates and re-initiates traffic. That’s why meshes can do mTLS, retries, timeouts, protocol-aware metrics, and consistent trace context propagation without touching application code. They also give you a very predictable place to emit telemetry because every request gets forced through the same choke point.&lt;/p&gt;

&lt;p&gt;eBPF-based, node-level observability is the opposite philosophy. It says: the kernel already sees what actually happened. Observe it once per node and stop stapling a proxy onto every workload.&lt;/p&gt;

&lt;p&gt;If you’ve ever had a cluster get wobbly because thousands of pods restarted and each one had to pull, init, and warm a sidecar, you already understand the appeal.&lt;/p&gt;

&lt;p&gt;What you typically &lt;strong&gt;gain&lt;/strong&gt; moving from sidecars to eBPF node agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer moving pieces per workload&lt;/strong&gt;. Less time debugging injection webhooks. Less “why is this namespace configured differently?” nonsense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower per-pod resource tax&lt;/strong&gt;. You stop paying the “every pod has a proxy” overhead in CPU, memory, and scheduling pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaner failure domains&lt;/strong&gt;. A bad proxy config doesn’t brick an entire deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster scale-out&lt;/strong&gt;. Fewer images to pull. Fewer containers to start. Less churn during HPA events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you &lt;strong&gt;lose&lt;/strong&gt; (or need to rebuild somewhere else):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mTLS as a default&lt;/strong&gt;. The sidecar is the easy enforcement point. Without it, you need another enforcement layer (CNI policy, node-level enforcement, or an ambient data plane).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L7 semantics&lt;/strong&gt;. Retries, consistent request/response metrics, protocol-aware routing. Proxies are good at this because they’re literally in the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application context&lt;/strong&gt;. User IDs, order IDs, feature flags, and any “what this request &lt;em&gt;means&lt;/em&gt;” metadata does not exist in the kernel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My stance: if you’re removing sidecars primarily for simplicity and cost, you’re probably right. If you’re removing sidecars because you think eBPF gives you “full service mesh observability,” you’re about to have a bad quarter.&lt;/p&gt;

&lt;p&gt;The mental model I use is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;eBPF is great at&lt;/strong&gt;: what happened (flows, connection attempts, latency at TCP/HTTP boundaries when parsable, DNS queries, drops).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidecars are great at&lt;/strong&gt;: what it meant and what we did about it (retries, route decisions, mTLS identity, request attributes).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which signals are reliable with eBPF, and what still needs in-process instrumentation
&lt;/h2&gt;

&lt;p&gt;The reliable eBPF signals are the ones the kernel can see without guessing.&lt;/p&gt;

&lt;p&gt;[YOUTUBE:8WCbGSCyDSo|Hubble - eBPF Based Observability for Kubernetes - Sebastian Wicki, Isovalent]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network flows (L3/L4)&lt;/strong&gt;: who talked to whom, over which port/protocol, how much data moved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS visibility&lt;/strong&gt;: queries and responses at the node boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection health&lt;/strong&gt;: SYN retries, resets, timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some HTTP visibility&lt;/strong&gt;: depending on the tool and config, you can often get method, path, status code, latency. But it’s inherently “best effort” because you’re reconstructing application semantics from kernel events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The quickest way to internalize this is to listen to how the Cilium ecosystem talks about it. In &lt;a href="https://www.youtube.com/watch?v=8WCbGSCyDSo" rel="noopener noreferrer"&gt;Sebastian Wicki&lt;/a&gt;’s CNCF talk on Hubble, the center of gravity is &lt;strong&gt;flows&lt;/strong&gt; and network-level visibility first. That’s the honest core of eBPF observability.&lt;/p&gt;

&lt;p&gt;What still requires in-process instrumentation (and always will):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Business spans&lt;/strong&gt;: “Checkout”, “FraudCheck”, “PlanUpgrade”. The kernel does not know your domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom attributes&lt;/strong&gt;: &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;cart_value&lt;/code&gt;, feature flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue semantics&lt;/strong&gt;: you can see network and syscalls, but “spent 40 seconds in topic X” is application knowledge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB query tags&lt;/strong&gt;: unless your client library emits spans/metrics, eBPF won’t tell you “this was query class Y from code path Z.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where teams faceplant: they treat “sidecarless” as “instrumentationless.” It’s not. You’re just shifting the baseline from per-pod proxy telemetry to per-node kernel telemetry.&lt;/p&gt;

&lt;p&gt;If you want a clean split of responsibilities, the pattern that keeps working is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;eBPF node agent&lt;/strong&gt; for baseline networking + golden signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry (OTel) in-process&lt;/strong&gt; for business traces and any attribute you’d ever use in a postmortem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re already on OTel, connect it to your eBPF signals instead of trying to pick a winner. My post on &lt;a href="https://dev.to/blog/opentelemetry-ai-agents-instrumentation"&gt;OpenTelemetry instrumentation&lt;/a&gt; is aimed at AI agents, but the discipline is the same for microservices: instrument what you’ll page on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kernel and managed-Kubernetes constraints (BTF, CO-RE, cgroup v2)
&lt;/h2&gt;

&lt;p&gt;This is the part people hand-wave in architecture reviews because it feels “platform-y.” Then the rollout hits reality.&lt;/p&gt;

&lt;p&gt;Sidecars are portable because they’re just containers. eBPF agents are only portable if the underlying kernel features are there.&lt;/p&gt;

&lt;p&gt;One important constraint here: the research tooling I use to pull validated kernel requirements and managed-Kubernetes matrices failed while generating this draft (the &lt;code&gt;web_search&lt;/code&gt; tool returned empty results). So I’m not going to invent a “kernel &amp;gt;= X.Y” claim and dress it up as certainty. That’s how bad infra decisions get justified.&lt;/p&gt;

&lt;p&gt;Here’s the checklist I actually use to evaluate eBPF viability on a real cluster:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kernel feature availability&lt;/strong&gt;: confirm the specific eBPF hooks your agent needs are enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BTF availability&lt;/strong&gt;: does the node kernel expose BTF data required for modern eBPF workflows?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CO-RE compatibility&lt;/strong&gt;: can your eBPF programs run across kernel versions without per-version rebuilds?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroup v2&lt;/strong&gt;: increasingly common and it changes enforcement/visibility models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSM / security posture&lt;/strong&gt;: some orgs lock down kernel capabilities hard. Know what your security team will actually allow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In managed Kubernetes, you don’t “just upgrade the kernel.” You upgrade node pools, AMIs, or managed OS images. That means your migration plan has to include infra owners from day one.&lt;/p&gt;

&lt;p&gt;If your org treats node images as snowflakes, fix that first. Otherwise you end up with a split-brain cluster where half the nodes are observable and the other half are dark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment patterns: DaemonSets, gateway-only proxies, and ambient mesh
&lt;/h2&gt;

&lt;p&gt;“Sidecarless” isn’t one architecture. It’s a spectrum.&lt;/p&gt;

&lt;p&gt;Here are the four patterns I see work in practice, from least disruptive to most.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Observe-only eBPF DaemonSet (no mesh change)
&lt;/h3&gt;

&lt;p&gt;This is the first move I’d make for almost any Kubernetes team. Run an eBPF agent as a DaemonSet and don’t touch the datapath.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: baseline your service graph and latency without changing traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk&lt;/strong&gt;: low. The agent can still burn CPU if you’re sloppy, but you’re not intercepting requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Partial mesh: keep sidecars for “hard requirements”
&lt;/h3&gt;

&lt;p&gt;Keep sidecars only where they’re doing something you can’t replace easily.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;workloads requiring strict &lt;strong&gt;mTLS identity&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;workloads doing &lt;strong&gt;L7 routing&lt;/strong&gt; / canarying via the mesh&lt;/li&gt;
&lt;li&gt;legacy services where you need a proxy to normalize telemetry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else gets node-level observability plus in-process tracing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Gateway-only proxies (edge + choke points)
&lt;/h3&gt;

&lt;p&gt;A lot of teams eventually admit they only needed proxy semantics at the boundary.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put Envoy/NGINX at &lt;strong&gt;ingress&lt;/strong&gt; and maybe &lt;strong&gt;egress&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use eBPF for east-west visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of those cases where the boring answer is actually the right one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Ambient mesh (sidecarless service mesh)
&lt;/h3&gt;

&lt;p&gt;Ambient mesh is the attempt to keep mesh-level security and policy while dropping per-pod sidecars. If you’re already deep into Istio (or similar), this can be a clean evolutionary path.&lt;/p&gt;

&lt;p&gt;I’m not going to embed vendor-specific claims here because, again, sources didn’t load. But the principle matters: split the mesh data plane into node-level components.&lt;/p&gt;

&lt;p&gt;Rule of thumb: don’t jump straight to ambient mesh if your real pain is “too many sidecars.” Start with observe-only and gateway-only. You’ll learn faster with less blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  A phased migration plan (observe-only → partial mesh → gateway-only → sidecarless)
&lt;/h2&gt;

&lt;p&gt;This is the plan I’d run with a real team, not a conference talk plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 0: Decide what you’re &lt;em&gt;actually&lt;/em&gt; trying to fix
&lt;/h3&gt;

&lt;p&gt;Write down the pain in numbers, not vibes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We have &lt;strong&gt;N&lt;/strong&gt; sidecars per node and they consume &lt;strong&gt;X%&lt;/strong&gt; of allocatable memory.”&lt;/li&gt;
&lt;li&gt;“Our P99 at ingress is &lt;strong&gt;Y ms&lt;/strong&gt;, but P99 between services is &lt;strong&gt;Z ms&lt;/strong&gt; and we can’t explain the gap.”&lt;/li&gt;
&lt;li&gt;“Deployments are slowed by sidecar init and image pull.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can’t quantify the pain, you can’t call the migration a win.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Add eBPF observe-only, build your service graph
&lt;/h3&gt;

&lt;p&gt;You want two outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a service dependency graph that matches reality&lt;/li&gt;
&lt;li&gt;a baseline latency distribution per edge (P50/P95/P99)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase usually produces the first uncomfortable truth. A bunch of “critical” calls aren’t critical. A bunch of “internal” calls are on the user path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Keep sidecars only where they provide a must-have control
&lt;/h3&gt;

&lt;p&gt;Most clusters end up with a minority of workloads that truly need proxy semantics. I usually see people land around 10–30%.&lt;/p&gt;

&lt;p&gt;Be ruthless. If a sidecar exists only because “that’s our template,” delete it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Move policy to the network layer, not the pod
&lt;/h3&gt;

&lt;p&gt;You can’t remove sidecars and then shrug about security.&lt;/p&gt;

&lt;p&gt;In practice, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enforce identity and policy with your CNI / network policy model&lt;/li&gt;
&lt;li&gt;use gateway proxies for boundary controls&lt;/li&gt;
&lt;li&gt;keep OTel in-process for business tracing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 4: Remove sidecars for the rest, with rollback criteria
&lt;/h3&gt;

&lt;p&gt;Rollback criteria is not “things feel weird.” It’s measurable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;P99 regression &amp;gt; &lt;strong&gt;X%&lt;/strong&gt; for &lt;strong&gt;Y minutes&lt;/strong&gt; on key endpoints&lt;/li&gt;
&lt;li&gt;increase in 5xx rate &amp;gt; &lt;strong&gt;Z&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;missing trace coverage over a threshold&lt;/li&gt;
&lt;li&gt;telemetry cost increase beyond budget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where I’ll borrow a lesson from my own work building this site’s publishing pipeline. I’ve learned the hard way that &lt;strong&gt;deterministic gates before LLM review catch more than doubling the review model’s size&lt;/strong&gt;. The same mindset applies to infra migrations. Put numbers in front of humans before humans debate.&lt;/p&gt;

&lt;p&gt;If you want a CI/CD angle on this, my post on &lt;a href="https://dev.to/blog/ai-code-review-github-actions"&gt;CI/CD&lt;/a&gt; is AI-themed, but the gating logic is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to validate P99 latency after removing sidecars
&lt;/h2&gt;

&lt;p&gt;Most teams do “remove sidecars, eyeball dashboards, ship it.” That’s how you end up reintroducing sidecars six weeks later, quietly, after an incident.&lt;/p&gt;

&lt;p&gt;Sidecars impact latency in two competing ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They &lt;strong&gt;add&lt;/strong&gt; hops and queues (serialization, filter chains, connection pools).&lt;/li&gt;
&lt;li&gt;They can &lt;strong&gt;reduce&lt;/strong&gt; tail latency by enforcing timeouts/retries consistently, or smoothing out client behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So don’t assume “removing sidecars makes it faster.” Measure it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benchmark design that doesn’t lie
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pick &lt;strong&gt;3–5 critical endpoints&lt;/strong&gt; that dominate user experience.&lt;/li&gt;
&lt;li&gt;Capture real traffic shape. If you can’t, replay production logs into a staging environment.&lt;/li&gt;
&lt;li&gt;Run at &lt;strong&gt;two load points&lt;/strong&gt;: normal peak (say &lt;strong&gt;60%&lt;/strong&gt; of known max) and stress (say &lt;strong&gt;90%&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Measure &lt;strong&gt;end-to-end&lt;/strong&gt; at ingress and &lt;strong&gt;hop-by-hop&lt;/strong&gt; between services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Golden signals to track during the canary
&lt;/h3&gt;

&lt;p&gt;Track at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latency (P50/P95/P99)&lt;/li&gt;
&lt;li&gt;error rate (4xx/5xx split)&lt;/li&gt;
&lt;li&gt;saturation (CPU throttling, run queue, network drops)&lt;/li&gt;
&lt;li&gt;retries/timeouts if applicable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Canary approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start with &lt;strong&gt;1 node pool&lt;/strong&gt; (or &lt;strong&gt;5%&lt;/strong&gt; of traffic)&lt;/li&gt;
&lt;li&gt;run for a full business cycle (often &lt;strong&gt;24–72 hours&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;expand only when your rollback triggers stayed quiet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re interested in tail latency mechanics more broadly, my Postgres write-up on &lt;a href="https://dev.to/blog/transparent-huge-pages-postgres-performance"&gt;P99 latency cliffs&lt;/a&gt; is a good reminder that “average is fine” is how incidents begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cardinality and telemetry cost: control it like a budget
&lt;/h2&gt;

&lt;p&gt;Cardinality is where “more observability” turns into “why is our bill 3x.” eBPF makes it easier to collect lots of dimensions because the kernel can see a ton of distinct tuples (src/dst, ports, pod IDs, DNS names). That’s useful. It’s also how you accidentally light money on fire.&lt;/p&gt;

&lt;p&gt;Treat telemetry like an API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Define allowed labels&lt;/strong&gt; for metrics. Anything else gets rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap high-cardinality fields&lt;/strong&gt;. Don’t emit &lt;code&gt;pod_uid&lt;/code&gt;, full URL query strings, or user IDs as metric labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample traces intentionally&lt;/strong&gt;. Head-based sampling for baseline volume. Tail-based sampling for error-heavy endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate at the edge&lt;/strong&gt;. Per-node aggregation can prevent hot-spotting your backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A concrete budget model that works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pick a top-line monthly telemetry budget (say &lt;strong&gt;$X&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;allocate &lt;strong&gt;50%&lt;/strong&gt; to metrics, &lt;strong&gt;30%&lt;/strong&gt; to logs, &lt;strong&gt;20%&lt;/strong&gt; to traces (tune for your org)&lt;/li&gt;
&lt;li&gt;decide what gets cut first when you exceed budget (usually high-cardinality metrics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also where I’ve become allergic to “we’ll figure it out later.” In the incident log for this blog’s publishing pipeline, one slug rewrite burned &lt;strong&gt;907K impressions&lt;/strong&gt; of link equity in one shot. One “small” operational decision. Massive compounding cost. Observability cardinality works the same way.&lt;/p&gt;

&lt;p&gt;For teams building &lt;a href="https://dev.to/pillars/ai-engineering-production"&gt;AI in production&lt;/a&gt;, telemetry cost discipline is already table stakes. Kubernetes observability should be held to the same bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take: sidecarless is the baseline, not the finish line
&lt;/h2&gt;

&lt;p&gt;The industry is drifting toward a layered model, because it matches reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node-level eBPF for “truth from the kernel” and cheap, ubiquitous visibility.&lt;/li&gt;
&lt;li&gt;In-process OpenTelemetry for business meaning.&lt;/li&gt;
&lt;li&gt;Proxies, but only where they’re genuinely doing policy work (ingress, egress, a few sensitive workloads).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hype pitch is “eBPF replaces meshes.” The real story is “eBPF makes meshes optional.” That’s a huge win if you’re tired of paying per-pod tax.&lt;/p&gt;

&lt;p&gt;My prediction: once Kubernetes teams treat sidecars as an exception rather than the default, the new operational bottleneck won’t be proxies. It’ll be governance. Who gets to emit which labels, at what cardinality, with what budget. The teams that treat telemetry like a product surface will move faster. Everyone else will keep rediscovering the same billing surprise, one incident at a time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/ebpf-observability-sidecars-kubernetes?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ebpf-observability-sidecars-kubernetes" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>observability</category>
      <category>kubernetes</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stacked PRs on GitHub [2026]: A Copy-Paste Workflow</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:45:10 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/stacked-prs-on-github-2026-a-copy-paste-workflow-1o8n</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/stacked-prs-on-github-2026-a-copy-paste-workflow-1o8n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/stacked-prs-github-workflow" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stacked PRs on GitHub are the fastest way I know to stop shipping-by-mega-PR without turning your team into a process cult. If you’re here for &lt;strong&gt;how to do stacked PRs on GitHub&lt;/strong&gt;, you probably have the same two problems every team hits: review fatigue (nobody wants your 1,800-line diff) and developer blocking (you’re “done” but can’t start the next slice until &lt;code&gt;main&lt;/code&gt; merges).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stacked pull requests work when every PR is reviewable alone, even if it can’t ship alone.&lt;/li&gt;
&lt;li&gt;A simple naming + title convention is 80% of the battle for dependent pull requests GitHub workflows.&lt;/li&gt;
&lt;li&gt;Changing a PR’s base branch is powerful, but it can make comments outdated and even drop commits from the timeline.&lt;/li&gt;
&lt;li&gt;CI for stacked PRs should be tiered (smoke per PR, full suite once per stack), or you’ll pay N× for the same signal.&lt;/li&gt;
&lt;li&gt;Merge the stack bottom-up, and have a restack routine ready for conflicts and base-branch drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Stacked PRs aren’t “more process”. They’re the same work, sliced so humans and CI can actually keep up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem with Large Pull Requests
&lt;/h2&gt;

&lt;p&gt;A big PR is a tax you pay in three currencies: reviewer attention, CI time, and your own momentum.&lt;/p&gt;

&lt;p&gt;I don’t care how good your team is. Once a diff gets huge, the odds of a serious review drop hard. People start skimming. They defer it. They ask for “quick calls” because reading it properly is too much. Then you end up shipping vibes, not software.&lt;/p&gt;

&lt;p&gt;Tomas Reimers (Co-founder/CEO at Graphite) uses a deliberately painful example: a single PR with &lt;strong&gt;~2,000 lines&lt;/strong&gt; added. The point isn’t that 2,000 lines is always wrong. The point is that it’s &lt;em&gt;predictably&lt;/em&gt; less likely to be reviewed well, or reviewed at all. That “I’ll look later” PR becomes the load-bearing wall for your whole feature.&lt;/p&gt;

&lt;p&gt;Then you do one of three things, none of them good:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You ping reviewers until you feel annoying (because you are).&lt;/li&gt;
&lt;li&gt;You context-switch to something else and lose the thread.&lt;/li&gt;
&lt;li&gt;Or you keep piling commits onto the same branch until the diff turns into a horror movie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stacked PRs exist because the classic “branch off &lt;code&gt;main&lt;/code&gt; only” mental model is mostly habit. Git doesn’t require it. GitHub doesn’t require it. Teams just got trained into it.&lt;/p&gt;

&lt;p&gt;GitHub making stacked PRs a first-class concept in docs (create/manage/review/CI/troubleshoot/merge) matters because it stops being tribal knowledge. You can point to a canonical workflow and say, “No, this isn’t some weird personal preference. It’s supported.”&lt;/p&gt;

&lt;p&gt;Here’s the contrarian part: &lt;strong&gt;the mechanics are easy&lt;/strong&gt;. What’s hard is hygiene. Without hygiene, stacked PRs just give you five smaller messes instead of one big mess.&lt;/p&gt;

&lt;p&gt;(Inline illustration suggestion: “One huge PR vs. 4 stacked PRs” diagram.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Stacked Pull Requests (and what they actually are)
&lt;/h2&gt;

&lt;p&gt;Stacked pull requests are multiple pull requests where each PR’s base branch is the PR below it, creating a dependency chain like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main &amp;lt;- PR1 &amp;lt;- PR2 &amp;lt;- PR3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Instead of one branch containing every commit for a feature, you split the feature into layers. Each layer is small enough to review and reason about. The stack still represents one coherent feature, but reviewers can approve incrementally.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are stacked pull requests?
&lt;/h3&gt;

&lt;p&gt;They’re &lt;strong&gt;dependent PRs&lt;/strong&gt; where the “compare against” branch is not &lt;code&gt;main&lt;/code&gt;, but another feature branch. GitHub shows each layer’s diff as “what changed since the base layer”. That’s the whole point.&lt;/p&gt;

&lt;p&gt;This is why people who’ve used Gerrit/Phabricator feel at home here. You’re reviewing a sequence of changes, not a monolith.&lt;/p&gt;

&lt;h3&gt;
  
  
  The simplest branch naming + PR title convention
&lt;/h3&gt;

&lt;p&gt;I’m opinionated here because ambiguity kills stacks. If your naming is sloppy, you force everyone to keep the whole stack in their head. That defeats the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch naming (copy-paste)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat/&amp;lt;ticket&amp;gt;-&amp;lt;feature&amp;gt;/01-foundation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat/&amp;lt;ticket&amp;gt;-&amp;lt;feature&amp;gt;/02-domain&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat/&amp;lt;ticket&amp;gt;-&amp;lt;feature&amp;gt;/03-api&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat/&amp;lt;ticket&amp;gt;-&amp;lt;feature&amp;gt;/04-ui&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat/123-comments/01-foundation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat/123-comments/02-storage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat/123-comments/03-endpoints&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat/123-comments/04-ui&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PR title format (copy-paste)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[1/4] Comments: foundation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[2/4] Comments: storage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[3/4] Comments: endpoints&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[4/4] Comments: UI&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why numbers? Because humans scan lists. Also, Slack links become readable. If you’ve ever tried to track “Comments PR” vs “Comments PR (follow-up)” vs “Comments PR final-final”, you know the pain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR description template (copy-paste)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; # → # → # → #&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depends on:&lt;/strong&gt; #&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unblocks:&lt;/strong&gt; #&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; What this PR includes (2–4 bullets)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-goals:&lt;/strong&gt; What this PR explicitly does not include&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review notes:&lt;/strong&gt; “Start with Files changed. Ignore generated files. Suggested order: X then Y.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a label like &lt;code&gt;stacked&lt;/code&gt; and optionally &lt;code&gt;stack:&amp;lt;ticket&amp;gt;&lt;/code&gt; so it’s filterable.&lt;/p&gt;

&lt;p&gt;Internal links you might want while you’re here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your team is already fighting review overload, my policy write-up on &lt;a href="https://dev.to/blog/ai-coding-team-workflow-policy-guide"&gt;code-review&lt;/a&gt; is the same problem from the other side.&lt;/li&gt;
&lt;li&gt;If your CI is already expensive, you’ll recognize the same pattern as eval gating in &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;CI/CD&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  GitHub PR mechanics that matter for stacks
&lt;/h2&gt;

&lt;p&gt;This is where GitHub-native stacks differ from the muscle memory you might have from Gerrit or Phabricator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Draft pull requests
&lt;/h3&gt;

&lt;p&gt;Use Draft PRs aggressively for the top of the stack.&lt;/p&gt;

&lt;p&gt;Rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bottom PR (&lt;code&gt;[1/4]&lt;/code&gt;) should be &lt;strong&gt;Ready for review&lt;/strong&gt; as soon as it has tests and a clean story.&lt;/li&gt;
&lt;li&gt;Everything above it starts life as a &lt;strong&gt;Draft pull request&lt;/strong&gt; until its base is stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drafts matter because they set expectations. They tell reviewers: “you can start looking, but don’t waste time trying to reason about CI failures that are just upstream churn.” GitHub supports drafts as a first-class state, and it cuts down on the pointless back-and-forth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull request refs and merge branches
&lt;/h3&gt;

&lt;p&gt;GitHub doesn’t just run checks on your head branch. It can create a synthetic merge ref (often described as a “merge branch”) representing “what would happen if we merged this PR into its base right now”. That’s the check you actually care about.&lt;/p&gt;

&lt;p&gt;For stacked PRs, this is non-negotiable because the base branch is itself changing.&lt;/p&gt;

&lt;p&gt;Your checks should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does PR3 work when merged into PR2’s branch &lt;em&gt;as it exists today&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;Or are we green only on the head branch while the merge result is broken?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use GitHub Actions required checks, you’re effectively requiring checks on that merge result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Differences between commits on compare and pull request pages
&lt;/h3&gt;

&lt;p&gt;GitHub shows commits differently depending on whether you’re looking at a compare view or the PR view. In stacks, this confuses people because commits can “move” after rebases/restacks.&lt;/p&gt;

&lt;p&gt;The fix is cultural, not technical: make review about &lt;em&gt;diffs and intent&lt;/em&gt;, not commit archaeology.&lt;/p&gt;

&lt;p&gt;Practical guidance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimize for &lt;strong&gt;Files changed&lt;/strong&gt; and a tight PR description.&lt;/li&gt;
&lt;li&gt;Treat commit order as a convenience, not a contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Collaborative development models
&lt;/h3&gt;

&lt;p&gt;Stacked PRs fit best with a “feature branch + PR” model, but they also work in trunk-based development if you treat each layer as a short-lived branch.&lt;/p&gt;

&lt;p&gt;The constraint is social, not technical: you’re asking reviewers to approve incremental work. If your team insists that every PR must be independently shippable and perfectly polished, stacks will feel “wrong”. If your team can handle “reviewable and correct, but not deployable alone”, stacks are great.&lt;/p&gt;

&lt;p&gt;GitHub’s foundational PR docs are worth reading once (not every week): &lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests" rel="noopener noreferrer"&gt;About pull requests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(Inline illustration suggestion: “PR tabs that matter: Conversation vs Checks vs Files changed”.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How to create dependent pull requests on GitHub (UI + CLI)
&lt;/h2&gt;

&lt;p&gt;This section is intentionally “do this now.”&lt;/p&gt;

&lt;h3&gt;
  
  
  UI workflow (GitHub-native)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create bottom branch from &lt;code&gt;main&lt;/code&gt;: &lt;code&gt;feat/123-comments/01-foundation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Push and open PR1 targeting &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create PR2 branch &lt;strong&gt;from PR1 branch&lt;/strong&gt;, not from &lt;code&gt;main&lt;/code&gt;: &lt;code&gt;feat/123-comments/02-storage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Push and open PR2 targeting &lt;strong&gt;PR1 branch&lt;/strong&gt; as the base.&lt;/li&gt;
&lt;li&gt;Repeat for PR3/PR4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do only one thing right: &lt;strong&gt;always branch from the previous layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you branch PR3 from &lt;code&gt;main&lt;/code&gt; “because it’s easier”, you didn’t build a stack. You built parallel PRs that will fight each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you stack pull requests with GitHub CLI?
&lt;/h3&gt;

&lt;p&gt;Yes. GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) doesn’t magically “manage stacks” for you, but it supports the mechanics just fine.&lt;/p&gt;

&lt;p&gt;A minimal flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use git locally to create branches off branches.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;gh pr create&lt;/code&gt; for each branch.&lt;/li&gt;
&lt;li&gt;Ensure the &lt;code&gt;--base&lt;/code&gt; points at the branch below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m not putting a giant command wall here because teams have different defaults and hooks. But the only two commands you need to remember are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gh pr create --base &amp;lt;base-branch&amp;gt; --head &amp;lt;head-branch&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh pr edit --base &amp;lt;new-base-branch&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a repeatable CLI environment for this kind of work, build muscle memory around clean tooling like &lt;a href="https://dev.to/blog/reproducible-terminal-dev-environment"&gt;direnv + mise&lt;/a&gt; so your git hooks, linters, and CI parity stay consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you change the base branch of a pull request?
&lt;/h3&gt;

&lt;p&gt;GitHub makes this a UI action and a CLI action, but it comes with sharp edges.&lt;/p&gt;

&lt;p&gt;GitHub explicitly warns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“When you change the base branch of your pull request, &lt;strong&gt;some commits may be removed from the timeline&lt;/strong&gt;.”&lt;/li&gt;
&lt;li&gt;“Review comments may also become &lt;strong&gt;outdated&lt;/strong&gt; because the line of code that the comment referenced may no longer be part of the changes.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source: &lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request" rel="noopener noreferrer"&gt;Changing the base branch of a pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also: when you open a PR, GitHub pins the base to the commit that branch references at that moment. If the base branch gets new commits later, GitHub &lt;strong&gt;does not automatically update&lt;/strong&gt; the base commit for your PR. That’s why stacks drift and suddenly your “green” PR isn’t really green anymore.&lt;/p&gt;

&lt;p&gt;Practical playbook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only change base branches when you’re restacking after a merge or conflict.&lt;/li&gt;
&lt;li&gt;When you change a base, leave a comment: “Restacked onto &lt;code&gt;&amp;lt;branch&amp;gt;&lt;/code&gt;; some threads may be outdated.”&lt;/li&gt;
&lt;li&gt;Expect reviewers to lose some context. Plan for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rebase vs merge: what to do with stacked PR branches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should stacked PR branches be rebased or merged?
&lt;/h3&gt;

&lt;p&gt;My stance: &lt;strong&gt;rebase locally, merge on GitHub&lt;/strong&gt;. Keep the PR diff clean, keep &lt;code&gt;main&lt;/code&gt; history sane, and don’t rewrite history other people are depending on.&lt;/p&gt;

&lt;p&gt;The canonical warning comes from Scott Chacon and Ben Straub in Pro Git: rebasing rewrites history and is dangerous on branches other people base work on. In stacks, that’s literally the point. Other branches are based on your branch.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://git-scm.com/book/en/v2/Git-Branching-Rebasing" rel="noopener noreferrer"&gt;Scott Chacon and Ben Straub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the real rule is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your stack is &lt;strong&gt;only you&lt;/strong&gt;, rebasing to keep diffs tight is fine.&lt;/li&gt;
&lt;li&gt;If a teammate is building on top of your stack branch, treat that branch as &lt;strong&gt;public&lt;/strong&gt;. Avoid rebasing it. Use merges or coordinate like adults.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete guidance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bottom PR (PR1)&lt;/strong&gt;: avoid force-pushing after review starts unless absolutely necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top PRs (PR3/PR4)&lt;/strong&gt;: rebase is more tolerable early, because fewer people reviewed and fewer branches depend on them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What happens to reviews when you rebase a stacked PR?
&lt;/h3&gt;

&lt;p&gt;Threads go stale. Approvals can be dismissed depending on branch protection settings. And even when GitHub keeps the “Approved” state, the human reality is that reviewers feel like they’re re-reviewing the same thing.&lt;/p&gt;

&lt;p&gt;The mitigation isn’t “never rebase.” The mitigation is discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep each layer small (think &lt;strong&gt;100–300 lines&lt;/strong&gt; net change, not a law but a useful target).&lt;/li&gt;
&lt;li&gt;Rebase early, not late.&lt;/li&gt;
&lt;li&gt;When you must rewrite history, do it once, do it cleanly, and tell people.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CI for stacked PRs with GitHub Actions (without paying N×)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you keep CI green with stacked PRs?
&lt;/h3&gt;

&lt;p&gt;You need to decide what “green” means at each layer.&lt;/p&gt;

&lt;p&gt;If you run the full suite on every PR in a 6-PR stack, you’ve built a CI cost multiplier. It’s not theoretical. It’s your cloud bill and your queue time.&lt;/p&gt;

&lt;p&gt;GitHub Actions gives you the primitives: the &lt;code&gt;pull_request&lt;/code&gt; event and filters (branches, paths) to decide when workflows run.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request" rel="noopener noreferrer"&gt;Events that trigger workflows – &lt;code&gt;pull_request&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a strategy that actually holds up in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tiered CI playbook (copy-paste policy)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Per-PR required checks (fast, deterministic)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lint + typecheck&lt;/li&gt;
&lt;li&gt;Unit tests for changed packages&lt;/li&gt;
&lt;li&gt;A “smoke” integration test that runs in &lt;strong&gt;&amp;lt;10 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These should be required on every PR in the stack, because they’re your “merge safety belt.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-stack full checks (slow, expensive)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full integration suite&lt;/li&gt;
&lt;li&gt;E2E tests&lt;/li&gt;
&lt;li&gt;Performance or load tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On PR1 only (the bottom), or&lt;/li&gt;
&lt;li&gt;Nightly on the stack label, or&lt;/li&gt;
&lt;li&gt;On merges to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re doing modern merge discipline, consider also running full checks on merge queue / merge group. Just don’t make every single PR pay for the entire test matrix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing CI duplication in practice
&lt;/h3&gt;

&lt;p&gt;You have three levers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Path filters&lt;/strong&gt;: don’t run backend integration tests when only docs changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt;: cancel in-progress runs when a new commit is pushed to the same PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required checks policy&lt;/strong&gt;: only require the smoke suite on stacked PRs; require full suite on merge-to-main.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same “regression gates” mindset I use in AI eval pipelines: cheap checks on every change, expensive checks on fewer choke points. If that resonates, &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;AI engineering eval gates&lt;/a&gt; is the same idea applied to prompts and tooling.&lt;/p&gt;

&lt;p&gt;(Inline illustration suggestion: “CI tiers per PR vs per stack” flowchart.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Review hygiene: how to keep reviewers sane
&lt;/h2&gt;

&lt;p&gt;Stacked PRs fail when reviewers don’t know what to do with them.&lt;/p&gt;

&lt;p&gt;And no, “just read them in order” isn’t enough. If you don’t set expectations, people will review PR4 like it’s PR1 and you’ll get philosophical debates in the UI layer. Every time.&lt;/p&gt;

&lt;p&gt;Here’s the reviewer contract that works.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should reviewers review a stack without re-reviewing the same diff multiple times?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: Review each PR as a layer, not as a feature.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR1: “Is the foundation right?”&lt;/li&gt;
&lt;li&gt;PR2: “Is the domain/model correct?”&lt;/li&gt;
&lt;li&gt;PR3: “Is the API behavior correct?”&lt;/li&gt;
&lt;li&gt;PR4: “Is the UI correct?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If someone comments “Why are we doing this?” in PR4, that’s a process smell. That belongs in PR1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: Don’t bikeshed naming in the middle of the stack.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you rename a core type in PR3, you just created churn across PR4 and PR5. Either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fix it in PR1/PR2, or&lt;/li&gt;
&lt;li&gt;Defer it to a follow-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mid-stack churn is how stacks become slower than mega-PRs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3: Approve with explicit dependency language.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use a standard comment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Approved &lt;strong&gt;assuming PR1 merges as-is&lt;/strong&gt;.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives the author permission to keep moving without pretending everything is independently shippable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 4: When a base changes, stop arguing with outdated threads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a thread is outdated, resolve it with a single note: “Restacked; this moved to PR2.” Then move on.&lt;/p&gt;

&lt;p&gt;If your team is also adopting AI-assisted coding and getting PR floods, you’re going to want a policy anyway. Start here: &lt;a href="https://dev.to/blog/ai-coding-team-workflow-policy-guide"&gt;AI coding team workflow policy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes: restacking, conflicts, and merging safely
&lt;/h2&gt;

&lt;p&gt;Stacks aren’t fragile. They’re just honest about dependency.&lt;/p&gt;

&lt;p&gt;You’re going to hit conflicts. You’re going to have to restack. That’s normal. The failure mode is pretending you don’t need a playbook and then reinventing one in a panic every sprint.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you restack after the bottom PR merges?
&lt;/h3&gt;

&lt;p&gt;When PR1 merges into &lt;code&gt;main&lt;/code&gt;, every PR above it is now based on the wrong branch.&lt;/p&gt;

&lt;p&gt;The clean GitHub-native approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update your local &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update your local PR2 branch by rebasing/merging onto &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Push PR2.&lt;/li&gt;
&lt;li&gt;Change PR2 base branch on GitHub from PR1 branch → &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Repeat upwards: PR3 base becomes PR2 branch, PR4 base becomes PR3 branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where GitHub’s warning matters: changing base can make comments outdated and drop commits from the timeline. That’s not a reason to avoid it. It’s a reason to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restack &lt;em&gt;quickly&lt;/em&gt; after merges.&lt;/li&gt;
&lt;li&gt;Avoid restacking daily as a hobby.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How do you handle mid-stack changes that require modifying lower layers?
&lt;/h3&gt;

&lt;p&gt;This is the classic: reviewer of PR4 finds an issue that should be fixed in PR2.&lt;/p&gt;

&lt;p&gt;Do not patch it in PR4.&lt;/p&gt;

&lt;p&gt;Do this instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the fix on PR2 branch.&lt;/li&gt;
&lt;li&gt;Merge/rebase PR3 and PR4 on top of PR2.&lt;/li&gt;
&lt;li&gt;Leave a short comment in PR4: “Fix moved to PR2; restacked.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, it’s extra work. It’s still less work than shipping a tangled diff where the real behavior change is hidden in the wrong layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you merge stacked PRs in order safely?
&lt;/h3&gt;

&lt;p&gt;Bottom-up. Always.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merge PR1 → &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Restack PR2 base to &lt;code&gt;main&lt;/code&gt; (or update it so it targets &lt;code&gt;main&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Merge PR2.&lt;/li&gt;
&lt;li&gt;Continue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use GitHub’s merge queue, stacks can work, but you need to be strict about required checks on each PR’s merge result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling: plain Git/GitHub vs GitHub CLI vs Graphite/Git Town
&lt;/h2&gt;

&lt;p&gt;You don’t need paid tooling to do stacked PRs. You need discipline.&lt;/p&gt;

&lt;p&gt;Here’s the decision table I’d use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;What you gain&lt;/th&gt;
&lt;th&gt;What you pay&lt;/th&gt;
&lt;th&gt;My take&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub UI only&lt;/td&gt;
&lt;td&gt;Small teams, low churn&lt;/td&gt;
&lt;td&gt;Zero setup&lt;/td&gt;
&lt;td&gt;Manual base changes&lt;/td&gt;
&lt;td&gt;Fine for stacks of 2–4 PRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub CLI (&lt;code&gt;gh&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Engineers who live in terminal&lt;/td&gt;
&lt;td&gt;Faster create/edit, scripting&lt;/td&gt;
&lt;td&gt;Still manual restacking logic&lt;/td&gt;
&lt;td&gt;Great default for most teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graphite / GitKraken&lt;/td&gt;
&lt;td&gt;High churn, lots of stacks&lt;/td&gt;
&lt;td&gt;Purpose-built stack ops, navigation&lt;/td&gt;
&lt;td&gt;Cost + workflow lock-in&lt;/td&gt;
&lt;td&gt;Worth it if stacks are daily life&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git Town&lt;/td&gt;
&lt;td&gt;Teams that want structured branching&lt;/td&gt;
&lt;td&gt;Consistent local branch ops&lt;/td&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Solid if you commit to it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want a good primer on why stacks exist and the “what if you didn’t have to branch off main?” framing, Tomas Reimers’ talk write-up is still useful context: &lt;a href="https://www.gitkraken.com/blog/stacked-pull-requests" rel="noopener noreferrer"&gt;Tomas Reimers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s the official talk video (worth 20 minutes):&lt;/p&gt;

&lt;p&gt;[YOUTUBE:U8wJoxxmskM|Stacked Pull Requests | GitKon 2022 | Tomas Reimers, Graphite]&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8-step stacked PR recipe (print this)
&lt;/h2&gt;

&lt;p&gt;This is the part you can paste into your team wiki.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slice the feature&lt;/strong&gt; into 3–6 layers (foundation → behavior → API → UI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create branches&lt;/strong&gt; with numbered names (&lt;code&gt;01-…&lt;/code&gt;, &lt;code&gt;02-…&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open PR1&lt;/strong&gt; targeting &lt;code&gt;main&lt;/code&gt;. Make it Ready for review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open PR2..PRn&lt;/strong&gt; targeting the branch below. Keep them Draft until stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI tiering:&lt;/strong&gt; require fast checks on every PR; run full suite once per stack or on merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer rules:&lt;/strong&gt; review the layer only; approve “assuming base merges”; avoid mid-stack churn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge bottom-up&lt;/strong&gt; only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restack after merges&lt;/strong&gt; by updating bases (expect some outdated threads; communicate clearly).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do this consistently, the surprising outcome isn’t just faster reviews. It’s better design. When you’re forced to name and isolate layers, bad abstractions show up early.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quick note on metrics
&lt;/h3&gt;

&lt;p&gt;If you want to prove this to your org, track two numbers for a month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Median PR “time to first review”&lt;/li&gt;
&lt;li&gt;Median PR “time to merge”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If stacked PRs don’t improve those, your bottleneck isn’t PR size. It’s staffing or ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: stacked PRs are a culture change disguised as branching
&lt;/h2&gt;

&lt;p&gt;GitHub making stacked PRs first-class is a hint: this workflow is no longer just a niche for teams that miss Phabricator.&lt;/p&gt;

&lt;p&gt;The teams that win in 2026 won’t be the ones with the fanciest tooling. They’ll be the ones who can keep &lt;strong&gt;review quality high&lt;/strong&gt; while &lt;strong&gt;shipping in thin slices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My prediction: within a year, “stack hygiene” will become as normal as “write tests.” Not because it’s trendy. Because it’s the only way to scale review when output keeps going up.&lt;/p&gt;

&lt;p&gt;If you try this workflow, be ruthless about one thing: &lt;strong&gt;don’t let stacks become an excuse to ship half-thought-through layers&lt;/strong&gt;. Small PRs are not inherently good. Reviewable PRs are good.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/stacked-prs-github-workflow?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=stacked-prs-github-workflow" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>codereview</category>
      <category>workflow</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Local LLM Break-Even Math [2026]: Power, Idle, Depreciation</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Fri, 14 Aug 2026 00:41:49 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/local-llm-break-even-math-2026-power-idle-depreciation-4fje</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/local-llm-break-even-math-2026-power-idle-depreciation-4fje</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/local-llm-break-even-cost-model" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Local LLM break even cost model power depreciation is the phrase I wish existed back when I first started seeing people justify $2–5k hardware purchases with “GPU price ÷ tokens.” That math is comforting. It’s also usually wrong.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that &lt;strong&gt;local LLM economics are dominated by what your GPU does when it’s not generating tokens&lt;/strong&gt;. Idle power. Low utilization. Depreciation. Random failures. The fact that you could have left the money in something boring and liquid instead.&lt;/p&gt;

&lt;p&gt;If you want a real answer to “Should I buy a GPU or just pay an API?”, you need a &lt;strong&gt;total cost of ownership&lt;/strong&gt; model. One you can copy into a spreadsheet and tweak with your own kWh price, duty cycle, and resale assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A local LLM cost model is mostly a utilization problem, not a “tokens per second” problem.&lt;/li&gt;
&lt;li&gt;Idle power can be a first-order cost if your rig sits on for 16–24 hours/day.&lt;/li&gt;
&lt;li&gt;Depreciation and resale value matter more than people admit, especially if you upgrade every 12–24 months.&lt;/li&gt;
&lt;li&gt;Break-even vs cloud depends on your workload shape (bursty vs steady), not your peak throughput.&lt;/li&gt;
&lt;li&gt;Opportunity cost is the adult version of “but I already bought the GPU.”&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If your GPU isn’t running hot most of the day, your “local is cheaper” math is probably a fantasy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this post is (and isn’t)
&lt;/h2&gt;

&lt;p&gt;This is not another simplistic calculator.&lt;/p&gt;

&lt;p&gt;It’s a &lt;strong&gt;parametric framework&lt;/strong&gt; for a local LLM total cost of ownership model that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed costs (hardware, depreciation, capital)&lt;/li&gt;
&lt;li&gt;Variable costs (electricity, maintenance)&lt;/li&gt;
&lt;li&gt;Real-world penalties (idle time, downtime, failure risk)&lt;/li&gt;
&lt;li&gt;A clean way to compare against cloud/API pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll also give you a ready-to-copy spreadsheet structure and three worked scenarios (hobbyist, indie, small team).&lt;/p&gt;

&lt;p&gt;I’m going to keep the numbers illustrative because this is meant to be &lt;strong&gt;2026-ready&lt;/strong&gt;. You swap in current pricing and rerun. Don’t hardcode 2024-era prices and pretend it’s “analysis.”&lt;/p&gt;

&lt;p&gt;For related baseline break-even thinking, I already have a simpler calculator-style post at &lt;a href="https://dev.to/blog/local-llm-cost-breakeven"&gt;Local LLM Cost vs Cloud API: 2026 Break-Even Math [Calculator]&lt;/a&gt;. This post is the grown-up version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive model everyone uses (and why it fails)
&lt;/h2&gt;

&lt;p&gt;The naive model is basically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buy GPU for $X&lt;/li&gt;
&lt;li&gt;GPU generates Y tokens/sec&lt;/li&gt;
&lt;li&gt;Cloud costs $Z per 1M tokens&lt;/li&gt;
&lt;li&gt;Therefore break-even occurs after N tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure modes are obvious once you say them out loud:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It assumes 100% utilization.&lt;/strong&gt; Your GPU is not generating tokens 24/7 unless you’re actually running a service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It ignores idle power.&lt;/strong&gt; Many rigs sit powered on all day because it’s your workstation, not a headless inference box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It ignores depreciation/resale.&lt;/strong&gt; GPUs are not servers you keep for 5 years. In local LLM land, people flip hardware quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It ignores failure risk.&lt;/strong&gt; A dead GPU fan or PSU turns your “cheap inference” into downtime and emergency spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It ignores opportunity cost.&lt;/strong&gt; Capital isn’t free, even if you paid cash.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you’re building &lt;a href="https://dev.to/pillars/llm-hardware-local-ai"&gt;local AI&lt;/a&gt; seriously, you need to think like FinOps, not like a Reddit comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TCO model: separate fixed vs variable costs
&lt;/h2&gt;

&lt;p&gt;Here’s the structure I use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define your time horizon
&lt;/h3&gt;

&lt;p&gt;Pick a horizon that matches reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12 months&lt;/strong&gt; if you upgrade often (common for local LLM hobbyists)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24–36 months&lt;/strong&gt; if you’re a small team buying “workhorse” machines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s call it &lt;code&gt;H&lt;/code&gt; months.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Fixed costs (hardware + capital)
&lt;/h3&gt;

&lt;p&gt;Fixed costs are things you pay regardless of utilization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware capital&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPU cost&lt;/li&gt;
&lt;li&gt;CPU/mobo/RAM/SSD cost (if this is a dedicated box)&lt;/li&gt;
&lt;li&gt;Networking/UPS (often ignored, sometimes critical)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Call total upfront cost &lt;code&gt;CapEx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depreciation (with resale)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the key: the cost is not what you paid. It’s:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DepreciationCost = CapEx - ResaleValue&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Model &lt;code&gt;ResaleValue&lt;/code&gt; as a percentage of CapEx after &lt;code&gt;H&lt;/code&gt; months.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best case: 70% resale after 12 months (hot market)&lt;/li&gt;
&lt;li&gt;Typical: 50%&lt;/li&gt;
&lt;li&gt;Worst: 20% (new generation drops, or your card is beat up)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Opportunity cost&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to be honest, add a cost of capital. Even 5% annual matters on a $4,000 build.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OpportunityCost ≈ CapEx × AnnualRate × (H/12)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the “I could have done literally anything else with that money” line item.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Variable costs (electricity + maintenance)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Electricity is two numbers:&lt;/strong&gt; idle draw and load draw.&lt;/p&gt;

&lt;p&gt;Define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;P_idle&lt;/code&gt; watts (system on, not generating)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;P_load&lt;/code&gt; watts (system generating tokens)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kWh_price&lt;/code&gt; ($/kWh)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EnergyCost = kWh_price × (P_idle×IdleHours + P_load×LoadHours) / 1000&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is where utilization finally shows up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance / incidentals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fans&lt;/li&gt;
&lt;li&gt;Thermal paste&lt;/li&gt;
&lt;li&gt;A replacement SSD&lt;/li&gt;
&lt;li&gt;That one power supply you regret buying&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Call it &lt;code&gt;MaintCost&lt;/code&gt; over the horizon. For a single rig, even &lt;strong&gt;$100–$300/year&lt;/strong&gt; is realistic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Downtime and failure risk
&lt;/h3&gt;

&lt;p&gt;For hobbyist rigs, downtime is “annoying.” For a small team, downtime is “you’re blocked.”&lt;/p&gt;

&lt;p&gt;A simple expected-value model works:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FailureRiskCost = FailureProbability × ReplacementCost&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can add a downtime penalty if this is production:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DowntimeCost = DowntimeHours × CostPerHour&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you’re doing &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI in production&lt;/a&gt; style workloads, you should price downtime honestly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Total local cost over horizon
&lt;/h3&gt;

&lt;p&gt;Put it together:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LocalTCO = DepreciationCost + OpportunityCost + EnergyCost + MaintCost + FailureRiskCost&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now you need an output metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you calculate local LLM cost per 1M tokens when GPU sits idle most of the day?
&lt;/h2&gt;

&lt;p&gt;You calculate it by &lt;strong&gt;dividing TCO by the tokens you actually generate&lt;/strong&gt;, not your theoretical maximum.&lt;/p&gt;

&lt;p&gt;Define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Throughput&lt;/code&gt; = tokens/sec when generating&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DutyCycle&lt;/code&gt; = fraction of time you’re generating tokens (0 to 1)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HoursOnPerDay&lt;/code&gt; = how long the machine is powered on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LoadHours = HoursOnPerDay × 30 × H × DutyCycle&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IdleHours = HoursOnPerDay × 30 × H × (1 - DutyCycle)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TotalTokens = Throughput × LoadHours × 3600&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Finally:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LocalCostPer1M = LocalTCO / (TotalTokens / 1,000,000)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the whole point: if &lt;code&gt;DutyCycle&lt;/code&gt; is 5%, your GPU is basically a space heater with a side hustle.&lt;/p&gt;

&lt;p&gt;If you want to sanity-check throughput assumptions, I publish reproducible local inference runs at &lt;a href="https://dev.to/llm-benchmarks"&gt;LLM benchmarks&lt;/a&gt;. One thing I’ve learned maintaining that database is that &lt;strong&gt;quantization quality cliffs are model-family-specific&lt;/strong&gt;. A blanket “just run Q4” recommendation is wrong, and it will change your real throughput and thus your cost per token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Utilization is the break-even lever (not peak performance)
&lt;/h2&gt;

&lt;p&gt;Here’s a simple intuition table. Assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizon: 24 months&lt;/li&gt;
&lt;li&gt;Machine on: 16 hours/day&lt;/li&gt;
&lt;li&gt;Idle draw: 80W&lt;/li&gt;
&lt;li&gt;Load draw: 450W&lt;/li&gt;
&lt;li&gt;Electricity: $0.15/kWh&lt;/li&gt;
&lt;li&gt;CapEx: $3,500&lt;/li&gt;
&lt;li&gt;Resale: 50% after 24 months&lt;/li&gt;
&lt;li&gt;Opportunity cost: 5% annual&lt;/li&gt;
&lt;li&gt;Maintenance + failure EV: $400 over horizon&lt;/li&gt;
&lt;li&gt;Throughput: 150 tok/s average&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can swap every number. The point is the shape.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Duty cycle (GPU generating)&lt;/th&gt;
&lt;th&gt;Tokens over 24 mo (approx)&lt;/th&gt;
&lt;th&gt;Energy cost (approx)&lt;/th&gt;
&lt;th&gt;Local cost per 1M tokens (approx)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;$300&lt;/td&gt;
&lt;td&gt;$80–$120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;126B&lt;/td&gt;
&lt;td&gt;$700&lt;/td&gt;
&lt;td&gt;$20–$35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;315B&lt;/td&gt;
&lt;td&gt;$1,300&lt;/td&gt;
&lt;td&gt;$8–$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;504B&lt;/td&gt;
&lt;td&gt;$1,900&lt;/td&gt;
&lt;td&gt;$5–$10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why the huge swing?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Depreciation is mostly fixed.&lt;/li&gt;
&lt;li&gt;Opportunity cost is fixed.&lt;/li&gt;
&lt;li&gt;Idle energy is fixed-ish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So your denominator (tokens) is what moves.&lt;/p&gt;

&lt;p&gt;This is also why “my GPU does 2x tok/s” is often less important than “my GPU is actually used.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What utilization rate makes buying a GPU cheaper than renting cloud GPUs?
&lt;/h2&gt;

&lt;p&gt;Break-even is when:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LocalCostPer1M ≤ CloudCostPer1M&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Cloud cost depends on whether you’re comparing to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LLM APIs&lt;/strong&gt; (pay per token)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud GPUs&lt;/strong&gt; (pay per hour)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;APIs are easier to compare. Cloud GPUs require you to build your own serving stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  API comparison (token pricing)
&lt;/h3&gt;

&lt;p&gt;You plug in your current provider’s $/1M input and output tokens. For a concrete anchor, Anthropic publicly listed Claude 3.5 Sonnet at &lt;strong&gt;$3 per million input tokens and $15 per million output tokens&lt;/strong&gt; in their announcement (&lt;a href="https://www.anthropic.com/news/claude-3-5-sonnet" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;But your blended $/1M depends on your ratio of input:output. A coding agent might generate a lot of output. A RAG Q&amp;amp;A flow might be input-heavy.&lt;/p&gt;

&lt;p&gt;If you want help thinking about token shapes and retries, see &lt;a href="https://dev.to/blog/ai-agent-cost-per-task-2026"&gt;AI Agent Cost Per Task [2026]: Token Budgets &amp;amp; Break-Even Math&lt;/a&gt; and &lt;a href="https://dev.to/blog/agent-per-task-cost-calculation"&gt;Agent Per-Task Cost Calculation [2026]: Retries, Tools, Caching&lt;/a&gt;. In production, retries dominate bills. I’ve seen that first-hand building the Firework short-video generation platform where &lt;strong&gt;the AI feature’s bill was dominated by retries and regeneration, not first-pass tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud GPU comparison (hourly pricing)
&lt;/h3&gt;

&lt;p&gt;Cloud GPUs can win if your workload is bursty. If you only need heavy inference 2 hours/day, renting is rational.&lt;/p&gt;

&lt;p&gt;For instance-type discovery and current on-demand rates, AWS’s instance family pages are the right canonical source, even if prices shift by region. Start with &lt;a href="https://aws.amazon.com/ec2/instance-types/g5/" rel="noopener noreferrer"&gt;AWS EC2 G5 instances&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Your cloud TCO will include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instance-hours × $/hour&lt;/li&gt;
&lt;li&gt;Storage&lt;/li&gt;
&lt;li&gt;Egress (often ignored)&lt;/li&gt;
&lt;li&gt;Ops time (absolutely ignored, but real)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re a small team, ops time is the silent killer. Every hour you spend babysitting drivers is an hour you’re not shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you account for depreciation and resale value of GPUs used for LLM inference?
&lt;/h2&gt;

&lt;p&gt;You should account for it explicitly, and you should do sensitivity analysis.&lt;/p&gt;

&lt;p&gt;I use three scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best&lt;/strong&gt;: You resell quickly, market stays hot. 60–70% resale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typical&lt;/strong&gt;: You keep it 2 years, sell when the next gen is out. 40–55% resale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worst&lt;/strong&gt;: You keep it 3 years or the card is effectively unsellable. 10–25% resale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two practical notes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Depreciation is not linear.&lt;/strong&gt; GPUs often cliff when a new generation drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local LLM usage can hurt resale.&lt;/strong&gt; If you ran it hot 24/7, buyers can tell.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you’re building a spreadsheet, make &lt;code&gt;Resale%&lt;/code&gt; a single input cell so you can slide it from 20% to 70% and watch your break-even move.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to model downtime, failures, and replacement costs for hobbyist rigs?
&lt;/h2&gt;

&lt;p&gt;Don’t overcomplicate it. Use expected value.&lt;/p&gt;

&lt;p&gt;For a hobbyist rig:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failure probability per year: 5–10% for “something annoying” (fan, PSU, SSD)&lt;/li&gt;
&lt;li&gt;Replacement cost: whatever you’d actually pay, not MSRP fantasies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FailureRiskCost = p_fail × cost_replace × years&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For a small team, add a downtime penalty:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a broken rig blocks 2 engineers for half a day, that’s 8 engineer-hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where local “cheap tokens” gets exposed. Tokens aren’t the expensive part. People are.&lt;/p&gt;

&lt;p&gt;If you’re serious about reliability, treat your local inference service like &lt;a href="https://dev.to/pillars/ai-agents"&gt;production AI&lt;/a&gt;. You need observability, limits, and safe deployment patterns. (And yes, this is why I write so much about that side of the house.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Opportunity cost: the line item everyone pretends doesn’t exist
&lt;/h2&gt;

&lt;p&gt;Opportunity cost is the difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buying a $4k box today&lt;/li&gt;
&lt;li&gt;Keeping that $4k liquid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can model it as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A conservative annual rate (4–6%)&lt;/li&gt;
&lt;li&gt;Or your company’s cost of capital&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even at 5% annual, $4,000 over 2 years is about &lt;strong&gt;$400&lt;/strong&gt;. That’s not nothing. It’s also not the biggest cost. But it’s the difference between “local wins” and “local barely ties” in a lot of real spreadsheets.&lt;/p&gt;

&lt;p&gt;If you hate the idea of opportunity cost, fine. Put the rate at 0%. Just don’t pretend you did math.&lt;/p&gt;

&lt;h2&gt;
  
  
  A spreadsheet template you can copy in 10 minutes
&lt;/h2&gt;

&lt;p&gt;Create these input cells:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizon months &lt;code&gt;H&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CapEx&lt;/li&gt;
&lt;li&gt;Resale %&lt;/li&gt;
&lt;li&gt;Annual opportunity rate&lt;/li&gt;
&lt;li&gt;kWh price&lt;/li&gt;
&lt;li&gt;Hours on per day&lt;/li&gt;
&lt;li&gt;P_idle (W)&lt;/li&gt;
&lt;li&gt;P_load (W)&lt;/li&gt;
&lt;li&gt;Duty cycle&lt;/li&gt;
&lt;li&gt;Throughput (tok/s)&lt;/li&gt;
&lt;li&gt;Maintenance cost (over horizon)&lt;/li&gt;
&lt;li&gt;Failure probability/year&lt;/li&gt;
&lt;li&gt;Replacement cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then compute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DepreciationCost&lt;/li&gt;
&lt;li&gt;OpportunityCost&lt;/li&gt;
&lt;li&gt;LoadHours, IdleHours&lt;/li&gt;
&lt;li&gt;EnergyCost&lt;/li&gt;
&lt;li&gt;FailureRiskCost&lt;/li&gt;
&lt;li&gt;LocalTCO&lt;/li&gt;
&lt;li&gt;TotalTokens&lt;/li&gt;
&lt;li&gt;LocalCostPer1M&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it. No magic.&lt;/p&gt;

&lt;p&gt;If you want a simpler version with fewer knobs, start with &lt;a href="https://dev.to/blog/reduce-llm-api-costs-production"&gt;LLM cost&lt;/a&gt; style API optimization first. If you can cut your API bill by 60% with caching and routing, that changes the whole break-even picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked scenarios: hobbyist, indie, small team
&lt;/h2&gt;

&lt;p&gt;These are not “the numbers.” They’re examples to show how the model behaves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario A: Hobbyist workstation (low utilization)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Machine on: 10 hours/day&lt;/li&gt;
&lt;li&gt;Duty cycle: 5–10%&lt;/li&gt;
&lt;li&gt;Throughput: 80–150 tok/s&lt;/li&gt;
&lt;li&gt;Horizon: 12 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Outcome: &lt;strong&gt;local cost per 1M tokens often looks worse than mid-tier APIs&lt;/strong&gt;, unless you have heavy daily usage.&lt;/p&gt;

&lt;p&gt;This is where most local rigs sit. People buy hardware for privacy and fun. That’s valid. Just don’t call it “cheaper.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario B: Indie builder shipping an agent (moderate utilization)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Machine on: 16 hours/day&lt;/li&gt;
&lt;li&gt;Duty cycle: 20–40%&lt;/li&gt;
&lt;li&gt;Horizon: 24 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Outcome: break-even becomes plausible. Your tokens amortize depreciation fast enough that local can beat “premium” APIs, especially if you’re generating lots of output.&lt;/p&gt;

&lt;p&gt;If you’re building &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt; that run continuously, you’re closer to a service profile. Utilization climbs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario C: Small team with a shared inference box (high utilization)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Machine on: 24/7&lt;/li&gt;
&lt;li&gt;Duty cycle: 50–80%&lt;/li&gt;
&lt;li&gt;Horizon: 24–36 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Outcome: local inference can get very cheap per 1M tokens. At this point, your main risks are reliability and ops overhead, not raw cost.&lt;/p&gt;

&lt;p&gt;This is also where you start caring about serving stacks (&lt;code&gt;vLLM&lt;/code&gt;, batching, queueing) and governance. A shared box is a shared blast radius.&lt;/p&gt;

&lt;p&gt;For serving tradeoffs, see &lt;a href="https://dev.to/blog/vllm-vs-ollama-production"&gt;vLLM vs Ollama 2026: Production Power or Developer Ease?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local vs cloud isn’t just cost: the hidden “soft” variables
&lt;/h2&gt;

&lt;p&gt;A purely numeric model still misses two real-world things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Privacy and data control.&lt;/strong&gt; If you can’t send data to a third party, cost doesn’t matter. Local wins by constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency and UX.&lt;/strong&gt; Local can be faster for tight loops. Cloud can be more consistent at scale.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to reason about latency properly, don’t use averages. Use streaming metrics. I wrote up how I think about that in &lt;a href="https://dev.to/blog/llm-latency-benchmark-methodology"&gt;LLM Latency Benchmark Methodology: Streaming UX Metrics [2026]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if you’re doing retrieval work, remember this: &lt;strong&gt;model choice is rarely the dominant factor&lt;/strong&gt;. When I built the Walmart conversational commerce chatbot at Firework, retrieval quality dominated answer quality at scale, and we were handling &lt;strong&gt;millions of queries daily&lt;/strong&gt; with &lt;strong&gt;sub-second responses&lt;/strong&gt;. That kind of system has a very different cost profile than a single local box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: the break-even question you should actually ask
&lt;/h2&gt;

&lt;p&gt;Most people ask: “When does my GPU pay for itself?”&lt;/p&gt;

&lt;p&gt;The better question is: &lt;strong&gt;“What workload do I need to justify keeping this GPU busy?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your answer is “I’ll use it sometimes,” you’re buying it because you want it. Own that. It’s a hobby. That’s fine.&lt;/p&gt;

&lt;p&gt;If your answer is “This will run daily, for real users, with real uptime expectations,” then do the spreadsheet, include depreciation and idle power, and make the call like an adult.&lt;/p&gt;

&lt;p&gt;My prediction for 2026: local LLM adoption will keep climbing, but the winners won’t be the people with the biggest GPUs. They’ll be the people who can keep them utilized, reliable, and boring. That’s where the actual cost advantage lives.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/local-llm-break-even-cost-model?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=local-llm-break-even-cost-model" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>localllm</category>
      <category>cost</category>
      <category>finops</category>
      <category>hardware</category>
    </item>
    <item>
      <title>OpenAI Codex Desktop Linux Install Guide [2026]: Sandbox + Data Egress</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Thu, 13 Aug 2026 12:44:59 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/openai-codex-desktop-linux-install-guide-2026-sandbox-data-egress-592h</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/openai-codex-desktop-linux-install-guide-2026-sandbox-data-egress-592h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/codex-desktop-linux-install-guide" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  OpenAI Codex Desktop Linux Install Guide [2026]: Sandbox + Data Egress
&lt;/h1&gt;

&lt;p&gt;Codex Desktop for Linux is the newest “AI coding agent on your machine” entry to hit the mainstream developer feed, and the search query I keep seeing is basically: &lt;strong&gt;openai codex desktop linux install guide&lt;/strong&gt;. Most posts stop at “it exists.” That’s useless. On Linux, the only parts that matter are packaging trust, sandboxing, and what leaves your network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Codex Desktop is a desktop agent running under your user account. Treat it like a powerful tool runner, not a cute chat UI.&lt;/li&gt;
&lt;li&gt;On Linux, the safest default is boring: verify the download, sandbox it, and keep filesystem access scoped to a single workspace directory.&lt;/li&gt;
&lt;li&gt;Assume anything you paste, any file you grant access to, and any command output you share can be uploaded. Don’t debate it. Measure it.&lt;/li&gt;
&lt;li&gt;Put a proxy in front of it if you care about enterprise controls. You want one choke point for egress and audit.&lt;/li&gt;
&lt;li&gt;If you can’t answer “what did it execute, what files did it read, and where did it connect,” you’re not operating it safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A desktop coding agent is not “just another app.” It’s automation with network access running in your user context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;(Inline illustration suggestion: a diagram showing “Codex Desktop → sandbox → workspace dir only → proxy → OpenAI/OpenRouter” with logs captured at each step.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickstart (the safe version)
&lt;/h2&gt;

&lt;p&gt;Linux desktop distribution still isn’t “solved.” Pretending otherwise is how people end up running unsigned binaries out of &lt;code&gt;~/Downloads&lt;/code&gt; with their entire home directory mounted. Your job here is simple: reduce blast radius.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install in 7 steps (works regardless of distro/package)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Download from an official source&lt;/strong&gt; (vendor site or official GitHub release). Avoid random repackaged builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify integrity&lt;/strong&gt;: if checksums/signatures are provided, verify them before you run anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a dedicated workspace directory&lt;/strong&gt; (example: &lt;code&gt;~/codex-work/&lt;/code&gt;) and keep it boring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a dedicated OS user&lt;/strong&gt; if you’re serious (example: &lt;code&gt;codex&lt;/code&gt;) so the agent doesn’t inherit your whole home directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with a sandbox&lt;/strong&gt;: Flatpak permissions, Firejail, or a VM. Pick the lightest thing that gives you control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with network visibility&lt;/strong&gt;: run with a proxy or at least capture traffic during first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then sign in&lt;/strong&gt; and connect it to a repo with non-sensitive code first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The HN thread about “ChatGPT Desktop (Codex Desktop) for Linux” immediately devolved into packaging + permissions anxiety. Good. That’s the right instinct. Here’s the thread: &lt;a href="https://news.ycombinator.com/item?id=49281916" rel="noopener noreferrer"&gt;HN item id=49281916&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Codex Desktop (and why Linux changes the risk)
&lt;/h2&gt;

&lt;p&gt;Codex Desktop is a local desktop application that can chat about code, read files you allow, and run developer tools/commands on your machine. That combination is the whole story. It’s not “just generating text.” It’s interacting with your filesystem and toolchain.&lt;/p&gt;

&lt;p&gt;Linux changes the risk profile for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Distribution is fragmented.&lt;/strong&gt; You might get an AppImage, a &lt;code&gt;.deb&lt;/code&gt;, an &lt;code&gt;.rpm&lt;/code&gt;, Flatpak, Snap, or the cursed “curl pipe bash” approach (don’t). Each format has different update semantics and a different sandbox story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux gives you real knobs.&lt;/strong&gt; If you’re willing to do a bit of work, you can lock an app down hard. Most people just… don’t.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also, the ecosystem around “Codex” isn’t tiny. The official &lt;code&gt;openai/codex&lt;/code&gt; repo shows &lt;strong&gt;six figures of GitHub stars and five figures of forks&lt;/strong&gt; (high drift, but still a real adoption signal). Source: &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;openai/codex&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing and running Codex CLI (official baseline)
&lt;/h2&gt;

&lt;p&gt;Even if you’re here for the Desktop app, you should know what OpenAI ships “in the open.”&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;openai/codex&lt;/code&gt; repository is OpenAI-maintained and includes a &lt;code&gt;Quickstart&lt;/code&gt; plus install/run instructions for a terminal-based Codex CLI. See: &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;openai/codex&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why do I care? Because the CLI repo is where you can sanity-check the &lt;strong&gt;security policy&lt;/strong&gt; and how OpenAI frames safe operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security policy (read it, don’t skim)
&lt;/h3&gt;

&lt;p&gt;OpenAI’s &lt;code&gt;openai/codex&lt;/code&gt; repo includes a &lt;code&gt;SECURITY.md&lt;/code&gt; with explicit guidance and a pointer to safe operation boundaries. The raw file is here: &lt;a href="https://raw.githubusercontent.com/openai/codex/main/SECURITY.md" rel="noopener noreferrer"&gt;OpenAI SECURITY.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One line Linux operators should pay attention to: it points to “Agent approvals &amp;amp; security” documentation and explicitly calls out sandboxing, approvals, and network controls as the intended boundary controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Codex with your ChatGPT plan
&lt;/h3&gt;

&lt;p&gt;The CLI docs include a section titled “Using Codex with your ChatGPT plan” (in the repo navigation/headings). Entitlements change. Plans change. Blog posts rot. I’m not publishing a brittle matrix that’s wrong in 60 days.&lt;/p&gt;

&lt;p&gt;My stance is operational: auth is an event. If you authenticate the desktop agent with your main account, you just tied a local tool runner to your primary identity. Use the minimum privileges that still lets you do your job.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Inline illustration suggestion: a “two identities” diagram: personal account vs work account, each with separate SSH keys + separate proxy policy.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I install OpenAI Codex Desktop on Linux? (package formats + verification)
&lt;/h2&gt;

&lt;p&gt;I can’t assume which packaging OpenAI is shipping the day you read this. The HN thread has people mad about AppImage, and other people mad about “system integration daemons.” Welcome to Linux.&lt;/p&gt;

&lt;p&gt;So here’s the install guidance that stays true even if the packaging changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Prefer a sandboxed distribution format when possible
&lt;/h3&gt;

&lt;p&gt;If there’s a Flatpak build, it’s usually the best default for desktop agents because you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portal-mediated filesystem access&lt;/li&gt;
&lt;li&gt;Easy permission inspection&lt;/li&gt;
&lt;li&gt;Straightforward revocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you must use AppImage or a native package, add your own sandboxing layer (Firejail/AppArmor/VM). Otherwise you’re trusting whatever it does in your user context. And yes, that includes reading your dotfiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Verify what you can actually verify
&lt;/h3&gt;

&lt;p&gt;Verification comes in tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best:&lt;/strong&gt; vendor provides a signed release and you verify the signature chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good:&lt;/strong&gt; vendor provides checksums and you verify the hash matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare minimum:&lt;/strong&gt; download over TLS from a first-party domain and keep the artifact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What not to do: treat “someone posted a mirror on a forum” as a supply chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Update strategy (don’t let auto-update surprise you)
&lt;/h3&gt;

&lt;p&gt;Auto-update is fine. Silent auto-update is not.&lt;/p&gt;

&lt;p&gt;If the app updates itself outside your package manager, you need at least two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A way to &lt;strong&gt;observe version changes&lt;/strong&gt; (log it, even if it’s a local text file)&lt;/li&gt;
&lt;li&gt;A way to &lt;strong&gt;roll back&lt;/strong&gt; if it breaks your sandbox assumptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Rise People, the big lesson from shipping SOC 2-oriented scaffolding tooling was that compliance baked into the workflow beats “we’ll review it later.” Treat desktop agent updates the same way. If you don’t know what version is running, you don’t have a system. You have vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Codex Desktop run in a sandbox on Linux? (and how to enforce one)
&lt;/h2&gt;

&lt;p&gt;People casually assume desktop apps are sandboxed now. On Linux, that’s only true if you installed it in a sandboxed container format (Flatpak/Snap) or you built a sandbox yourself.&lt;/p&gt;

&lt;p&gt;Here’s how I think about the options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flatpak:&lt;/strong&gt; best balance for most engineers. Use portals + overrides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firejail:&lt;/strong&gt; great for AppImage and random binaries. Fast to apply, decent defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AppArmor/SELinux:&lt;/strong&gt; powerful, but you need patience and you’ll fight policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VM (or a disposable dev VM):&lt;/strong&gt; if you’re dealing with sensitive repos, this is the boring right answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote a deeper variant of the VM approach here: &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt; and specifically &lt;a href="https://dev.to/blog/ai-agent-sandbox-linux-vm"&gt;AI Agent Sandbox Linux VM [2026]: Safe Tool Use, No K8s&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flatpak permissions: the portal model you should embrace
&lt;/h3&gt;

&lt;p&gt;If you’re on Flatpak, inspect and minimize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filesystem mounts (home, host, specific paths)&lt;/li&gt;
&lt;li&gt;Device access&lt;/li&gt;
&lt;li&gt;Network access (usually on)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key control is scope. Your goal is: the agent sees &lt;code&gt;~/codex-work/&lt;/code&gt; and basically nothing else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firejail profile: blunt instrument, still effective
&lt;/h3&gt;

&lt;p&gt;Firejail isn’t magic. But it’s a solid “make it annoying to escape” layer for AppImage-style installs.&lt;/p&gt;

&lt;p&gt;Your initial success criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No access to &lt;code&gt;~/.ssh&lt;/code&gt; unless you explicitly allow it&lt;/li&gt;
&lt;li&gt;No access to &lt;code&gt;~/.gnupg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No access to &lt;code&gt;~/.aws&lt;/code&gt;, &lt;code&gt;~/.kube&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Read/write only within a workspace dir&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re writing a team policy, Firejail is the quick win.&lt;/p&gt;

&lt;h2&gt;
  
  
  What files can Codex Desktop read on my machine?
&lt;/h2&gt;

&lt;p&gt;The honest answer: &lt;strong&gt;whatever your OS permissions and the app sandbox allow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So stop debating what Codex “should” do and define your boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workspace-only:&lt;/strong&gt; the agent can read/write within a single project directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo-only:&lt;/strong&gt; it can read the repo but not your home directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full home:&lt;/strong&gt; don’t do this unless you genuinely accept the risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Credential hygiene (SSH, GPG, cloud creds)
&lt;/h3&gt;

&lt;p&gt;If the agent can read &lt;code&gt;~/.ssh&lt;/code&gt;, it can often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enumerate keys&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ssh-agent&lt;/code&gt; sockets if accessible&lt;/li&gt;
&lt;li&gt;Perform git operations as you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hardening moves that actually work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;separate SSH keys per repo or per trust domain&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Use a separate &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; for the agent environment&lt;/li&gt;
&lt;li&gt;Put work keys on a hardware token if you can&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re doing “vibe coding” with an agent that can see your production AWS creds in &lt;code&gt;~/.aws/credentials&lt;/code&gt;, you’re not moving fast. You’re gambling.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://dev.to/pillars/ai-security"&gt;AI security&lt;/a&gt; and &lt;a href="https://dev.to/blog/ai-agent-tool-use-security-attack-surface-checklist-2026"&gt;AI Agent Tool Use Security Attack Surface Checklist [2026]&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Codex Desktop upload my code to OpenAI servers? (and how to reason about data egress)
&lt;/h2&gt;

&lt;p&gt;If the desktop agent is using a hosted model, &lt;strong&gt;some data leaves your machine&lt;/strong&gt;. The interesting questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exactly &lt;em&gt;what&lt;/em&gt; leaves (full files vs diffs vs snippets)&lt;/li&gt;
&lt;li&gt;when it leaves (on open vs on request)&lt;/li&gt;
&lt;li&gt;whether telemetry/crash logs include code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I prefer a test-driven posture: assume worst, then verify with network capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  “What data leaves your machine” table (practical threat model)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;When it can be sent&lt;/th&gt;
&lt;th&gt;How to limit it&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompts you type&lt;/td&gt;
&lt;td&gt;Every request&lt;/td&gt;
&lt;td&gt;Don’t paste secrets. Build redaction habits.&lt;/td&gt;
&lt;td&gt;Capture HTTPS destinations + request sizes via proxy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code snippets/diffs&lt;/td&gt;
&lt;td&gt;When you ask it to edit/understand files&lt;/td&gt;
&lt;td&gt;Restrict filesystem scope to workspace.&lt;/td&gt;
&lt;td&gt;Compare file reads to outbound bursts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal output&lt;/td&gt;
&lt;td&gt;When you share command results&lt;/td&gt;
&lt;td&gt;Don’t run commands that print secrets, then hand the output to the agent.&lt;/td&gt;
&lt;td&gt;Log tool execution + capture outbound.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo metadata (paths, filenames)&lt;/td&gt;
&lt;td&gt;When agent indexes context&lt;/td&gt;
&lt;td&gt;Limit directory access. Use a clean workspace dir.&lt;/td&gt;
&lt;td&gt;Watch for directory listing activity + traffic spikes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash reports/telemetry&lt;/td&gt;
&lt;td&gt;On crash or opt-in telemetry&lt;/td&gt;
&lt;td&gt;Disable telemetry if possible; block endpoints.&lt;/td&gt;
&lt;td&gt;Block domains; verify no egress to telemetry hosts.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This isn’t fear-mongering. It’s basic &lt;a href="https://dev.to/glossary/llm"&gt;LLM security&lt;/a&gt; hygiene.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I restrict Codex Desktop’s filesystem access?
&lt;/h2&gt;

&lt;p&gt;There are only three strategies that reliably work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run it as a different Linux user&lt;/strong&gt; with a minimal home directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a sandbox&lt;/strong&gt; (Flatpak portals or Firejail) and only mount the workspace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a VM&lt;/strong&gt; and treat it like a disposable coding environment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The “different user” approach is underrated because it’s boring and it works across distros.&lt;/p&gt;

&lt;p&gt;A policy I like for internal tools is default-deny, then grant access one directory at a time. At Rise People, our SOC 2 scaffolding CLI succeeded because the defaults nudged engineers into compliant paths instead of relying on PR reviews and good intentions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I set a proxy for Codex Desktop (HTTPS proxy)?
&lt;/h2&gt;

&lt;p&gt;If you want enterprise-style controls (audit, DLP, allowlists), you want a proxy choke point.&lt;/p&gt;

&lt;p&gt;Two patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System proxy:&lt;/strong&gt; set environment variables like &lt;code&gt;HTTPS_PROXY&lt;/code&gt;/&lt;code&gt;HTTP_PROXY&lt;/code&gt; for the app process (method varies by launch mechanism).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network-level enforcement:&lt;/strong&gt; firewall rules or an outbound tool like OpenSnitch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re routing model traffic through a gateway, OpenRouter is a good mental model for “OpenAI-compatible” APIs. Their docs describe it as “hundreds of AI models through a single API endpoint,” and they show OpenAI-SDK integration as one path. See &lt;a href="https://openrouter.ai/docs" rel="noopener noreferrer"&gt;OpenRouter docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’m intentionally not claiming “all SDKs work by only swapping base URLs.” The tighter claim is: &lt;strong&gt;many OpenAI SDKs can be configured to point at a different &lt;code&gt;base_url&lt;/code&gt;, and OpenRouter documents OpenAI-SDK usage as one integration path.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I observe/confirm what network requests Codex Desktop makes?
&lt;/h2&gt;

&lt;p&gt;You have three practical options on Linux:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Packet capture&lt;/strong&gt; (&lt;code&gt;tcpdump&lt;/code&gt;/Wireshark): good for destinations, timing, volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit proxy&lt;/strong&gt; (mitmproxy or corporate proxy): best for audit and allowlists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound firewall prompts&lt;/strong&gt; (OpenSnitch): best for interactive control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn’t to decrypt everything. It’s to answer three operator questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which domains/IPs does it talk to?&lt;/li&gt;
&lt;li&gt;How often?&lt;/li&gt;
&lt;li&gt;How big are requests when it’s “reading” a repo?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your org is serious about &lt;a href="https://dev.to/pillars/production-ai"&gt;production AI&lt;/a&gt;, this is non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Inline illustration suggestion: screenshot-style checklist “traffic capture on first run” with domains, timestamps, request sizes.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where are Codex Desktop logs stored on Linux? (and what you should log yourself)
&lt;/h2&gt;

&lt;p&gt;Exact paths vary by packaging and build, so I’m not going to pretend there’s one canonical list. On Linux, desktop apps commonly write under:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.config/…&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.local/share/…&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.cache/…&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What matters more: keep your own minimal “paper trail,” regardless of where the app logs.&lt;/p&gt;

&lt;p&gt;A useful local audit trail for a desktop agent has three components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Process execution log&lt;/strong&gt; (what commands/tools did it run)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem access boundaries&lt;/strong&gt; (what directories were even mounted/visible)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network egress record&lt;/strong&gt; (where did it connect)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Incident writeups are basically endless proof that this matters. Tailscale’s August 12, 2026 post, “How we tracked down a 16-year-old SQLite bug,” is a love letter to forensics and traceability. They describe “months of intense forensics” to isolate a deep bug. Source: &lt;a href="https://tailscale.com/blog/sqlite-wal-reset-bug" rel="noopener noreferrer"&gt;Tailscale blog post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you can’t reconstruct what happened, you don’t have a secure posture. You have a hope posture.&lt;/p&gt;

&lt;p&gt;For a deeper agent logging schema (OpenTelemetry + redaction), see: &lt;a href="https://dev.to/blog/ai-agent-observability-logging-schema-2026"&gt;AI Agent Observability Logging Schema [2026]: OTel + Redaction&lt;/a&gt; and &lt;a href="https://dev.to/blog/opentelemetry-ai-agents-instrumentation"&gt;OpenTelemetry Instrumentation for AI Agents [2026]: Ship It&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I disable telemetry/crash reporting in Codex Desktop?
&lt;/h2&gt;

&lt;p&gt;Treat telemetry control as a three-layer problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In-app settings&lt;/strong&gt; (if offered)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS-level blocks&lt;/strong&gt; (hosts/firewall allowlists)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy policy&lt;/strong&gt; (best for teams)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And even if you “disable telemetry,” validate it with your own traffic capture. Settings UIs lie all the time. Sometimes accidentally. Sometimes because nobody bothered to test the edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I use separate SSH keys with Codex Desktop for Git operations?
&lt;/h2&gt;

&lt;p&gt;Yes. And you should.&lt;/p&gt;

&lt;p&gt;Two practical approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-repo SSH config:&lt;/strong&gt; map hostnames or repo paths to specific &lt;code&gt;IdentityFile&lt;/code&gt; entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate agent environment:&lt;/strong&gt; run Codex Desktop in an environment where &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; points to a restricted agent (or no agent at all).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is simple: the agent shouldn’t inherit your keys to the kingdom. If it needs push access, give it a deploy key scoped to one repo. Not your personal key that can push everywhere.&lt;/p&gt;

&lt;p&gt;This ties directly to broader &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt; hygiene and &lt;a href="https://dev.to/blog/ai-agent-threat-model-attack-surface"&gt;AI Agent Threat Model: 7 Attack Vectors [2026]&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team policy checklist (the minimum I’d ship)
&lt;/h2&gt;

&lt;p&gt;If you’re rolling Codex Desktop out to a team, here’s the baseline policy I’d want written down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Packaging:&lt;/strong&gt; only install from approved sources. Keep a record of version and artifact hash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox:&lt;/strong&gt; Flatpak or Firejail required. Workspace-only mounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials:&lt;/strong&gt; separate SSH keys. No shared tokens in env vars. No production AWS creds on dev boxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; proxy required for company repos. All egress logged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability:&lt;/strong&gt; log tool executions and keep logs for at least &lt;strong&gt;30 days&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop:&lt;/strong&gt; anything that writes to git needs explicit approval, especially force pushes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agent can push to &lt;code&gt;main&lt;/code&gt;, you’re one sloppy approval away from an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: the prediction
&lt;/h2&gt;

&lt;p&gt;Linux is going to become the most controllable environment for agentic desktop tooling. Not because it’s “more secure by default,” but because it gives you knobs that actually work.&lt;/p&gt;

&lt;p&gt;My prediction for the next 12 months: the teams that win with Codex Desktop (or any desktop agent) won’t be the ones with the best prompts. They’ll be the ones who can answer, on demand, &lt;strong&gt;what ran, what it touched, and what left the machine&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/codex-desktop-linux-install-guide?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=codex-desktop-linux-install-guide" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>codex</category>
      <category>aicoding</category>
      <category>desktopapp</category>
    </item>
    <item>
      <title>AI Agent Sandbox Linux VM [2026]: Safe Tool Use, No K8s</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Thu, 13 Aug 2026 00:42:35 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/ai-agent-sandbox-linux-vm-2026-safe-tool-use-no-k8s-486b</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/ai-agent-sandbox-linux-vm-2026-safe-tool-use-no-k8s-486b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/ai-agent-sandbox-linux-vm" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI agent sandbox linux vm setups are the fastest way to turn “LLM tool use” from a cool demo into something you can run without sweating every command. The moment your agent can execute a shell, install packages, or &lt;code&gt;curl&lt;/code&gt; the internet, you’ve created a tiny production-incident generator. My stance is simple: &lt;strong&gt;one disposable Linux VM per agent run&lt;/strong&gt;, with default-deny network egress, snapshot rollback, and auditable logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat an agent run like running untrusted code. Because functionally, that’s what it is.&lt;/li&gt;
&lt;li&gt;Default-deny outbound egress is the only policy that survives prompt injection and supply-chain surprises.&lt;/li&gt;
&lt;li&gt;Use a base image plus copy-on-write overlays so every run resets in seconds.&lt;/li&gt;
&lt;li&gt;Inject secrets as short-lived, least-privilege credentials. Don’t bake them into images. Don’t leave them on disk.&lt;/li&gt;
&lt;li&gt;Persist only what you can defend in a post-incident review: artifacts and audit bundles. Throw the rest away.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Give your agent tools only inside a disposable VM, and treat the VM like it’s already compromised.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Inline illustration suggestion: Diagram showing “Agent Orchestrator” launching a disposable VM, with arrows for allowlisted egress, artifact export, and log bundle export.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an AI agent sandbox?
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;AI agent sandbox&lt;/strong&gt; is an isolated execution environment that lets an agent use real tools (shell, Git, package managers, browsers) while limiting blast radius: file access, network access, and credentials. In practice, it’s a box you’re comfortable letting get owned.&lt;/p&gt;

&lt;p&gt;When people say “sandbox,” they often mean “a Docker container with vibes.” That’s fine for toy demos. For serious tool use, I want something that actually holds up when the agent goes off-script.&lt;/p&gt;

&lt;p&gt;At minimum, I’m looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A hard boundary (VM or VM-like isolation)&lt;/li&gt;
&lt;li&gt;A repeatable, disposable filesystem&lt;/li&gt;
&lt;li&gt;Strict outbound network controls&lt;/li&gt;
&lt;li&gt;A clean secrets story&lt;/li&gt;
&lt;li&gt;Forensics: who ran what, changed what, and talked to what&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt; that touch the outside world, sandboxing is not a “later” problem. It’s the entry price.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 8-step checklist I use as the mental model
&lt;/h3&gt;

&lt;p&gt;This is the minimal loop that keeps you out of trouble:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new VM instance (per run).&lt;/li&gt;
&lt;li&gt;Attach a fresh copy-on-write root overlay on top of a read-only base image.&lt;/li&gt;
&lt;li&gt;Configure networking on a dedicated interface (tap/bridge).&lt;/li&gt;
&lt;li&gt;Apply default-deny egress rules on that interface.&lt;/li&gt;
&lt;li&gt;Inject short-lived secrets at boot (scoped to this run).&lt;/li&gt;
&lt;li&gt;Run the agent’s tool loop through a single tool-runner entrypoint.&lt;/li&gt;
&lt;li&gt;Export artifacts and a log bundle.&lt;/li&gt;
&lt;li&gt;Destroy the VM and wipe overlays.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do only one thing from this post, do #4. Default-deny egress changes the whole risk profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why letting an agent run tools is uniquely risky
&lt;/h2&gt;

&lt;p&gt;“Untrusted code execution” used to be a special event. With agents, you’ve productized it.&lt;/p&gt;

&lt;p&gt;The failure modes in real agentic workflows are not subtle. They’re the obvious stuff we all learned to fear, except now it’s automated and fast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection → tool misuse.&lt;/strong&gt; The model gets talked into running commands it shouldn’t. If you haven’t internalized this yet, read my &lt;a href="https://dev.to/blog/prompt-injection-2026-owasp-llm-vulnerability"&gt;prompt injection&lt;/a&gt; post and my broader &lt;a href="https://dev.to/blog/ai-security-complete-guide"&gt;AI security&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply-chain installs.&lt;/strong&gt; The agent &lt;code&gt;pip install&lt;/code&gt;s or &lt;code&gt;npm install&lt;/code&gt;s something sketchy because it “fixed the build.” If you’re thinking “we pin versions,” congrats. That’s step 0, not the solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential theft via environment/process.&lt;/strong&gt; Agents and tools love environment variables. Malware loves them more. &lt;code&gt;/proc&lt;/code&gt; visibility and sloppy secret injection are how you lose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data exfiltration via outbound HTTP.&lt;/strong&gt; If the agent can talk to the internet, it can leak. The easiest exfil path is the one you already gave it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accidental destruction.&lt;/strong&gt; It’s not always malicious. &lt;code&gt;rm -rf&lt;/code&gt;, recursive edits, or “clean up this directory” on the wrong mount happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agent has a shell, it’s a junior engineer with root and zero judgment. Sandbox accordingly.&lt;/p&gt;

&lt;p&gt;A concrete number to make this feel less hand-wavy: Firecracker’s whole pitch is density and speed because this pattern is meant to run at scale. The project site says it can start user space in &lt;strong&gt;as little as 125 ms&lt;/strong&gt;, create up to &lt;strong&gt;150 microVMs per second per host&lt;/strong&gt;, and add &lt;strong&gt;&amp;lt;5 MiB&lt;/strong&gt; memory overhead per microVM. That’s the runtime telling you “disposable per-task VMs are not crazy.”&lt;/p&gt;

&lt;p&gt;(Those numbers are from the official &lt;a href="https://firecracker-microvm.github.io/" rel="noopener noreferrer"&gt;Firecracker&lt;/a&gt; site.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation runtimes: pick your poison
&lt;/h2&gt;

&lt;p&gt;There’s no perfect isolation. There are only tradeoffs you understand and can operate.&lt;/p&gt;

&lt;p&gt;For solo devs and small teams, I keep the shortlist simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux containers (runc)&lt;/li&gt;
&lt;li&gt;gVisor&lt;/li&gt;
&lt;li&gt;Kata Containers&lt;/li&gt;
&lt;li&gt;Firecracker microVMs&lt;/li&gt;
&lt;li&gt;Full-fat VMs (QEMU, VMware, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes, Kubernetes can orchestrate some of this. But you asked for “no Kubernetes required,” and I agree with the premise. K8s is great at scheduling. It does not magically solve default-deny egress, secrets lifecycle, or audit bundles. You still have to do the hard parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Comparison Table
&lt;/h3&gt;

&lt;p&gt;Here’s the table I wish existed back when everyone first told me “just run the agent in Docker”:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Isolation boundary&lt;/th&gt;
&lt;th&gt;Cold start&lt;/th&gt;
&lt;th&gt;Egress control ergonomics&lt;/th&gt;
&lt;th&gt;Snapshot/rollback ergonomics&lt;/th&gt;
&lt;th&gt;Best fit for agent sandboxes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Containers (runc)&lt;/td&gt;
&lt;td&gt;Shared kernel&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;td&gt;Easy-ish (netns/iptables), but mistakes leak&lt;/td&gt;
&lt;td&gt;Layered FS, but state leaks through mounts&lt;/td&gt;
&lt;td&gt;Lowest friction, highest foot-guns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gVisor&lt;/td&gt;
&lt;td&gt;User-space kernel layer&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Similar to containers, extra guardrails&lt;/td&gt;
&lt;td&gt;Similar to containers&lt;/td&gt;
&lt;td&gt;Better than runc when you can accept compat gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kata Containers&lt;/td&gt;
&lt;td&gt;VM-backed containers&lt;/td&gt;
&lt;td&gt;Slower than runc&lt;/td&gt;
&lt;td&gt;VM networking patterns&lt;/td&gt;
&lt;td&gt;VM disk patterns&lt;/td&gt;
&lt;td&gt;When you want “container UX, VM boundary”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecracker&lt;/td&gt;
&lt;td&gt;MicroVM (KVM)&lt;/td&gt;
&lt;td&gt;Fast for VMs (125 ms claim)&lt;/td&gt;
&lt;td&gt;Clean per-VM interface&lt;/td&gt;
&lt;td&gt;Great with overlays/snapshots&lt;/td&gt;
&lt;td&gt;Strong default for server-side “one run, one VM”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full VM (QEMU)&lt;/td&gt;
&lt;td&gt;VM (KVM optional)&lt;/td&gt;
&lt;td&gt;Typically slower&lt;/td&gt;
&lt;td&gt;Fine, but heavier&lt;/td&gt;
&lt;td&gt;Fine, but heavier&lt;/td&gt;
&lt;td&gt;When you need maximum compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your threat model includes “agent might run arbitrary code from the internet,” VM-backed isolation is the boring answer that’s actually right.&lt;/p&gt;

&lt;p&gt;Also: don’t over-rotate on “most secure.” The real question is “most secure that you can operate consistently.” A flaky security control is just a future incident with better marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: default deny is the only way
&lt;/h2&gt;

&lt;p&gt;Allowlisting outbound traffic feels annoying until you’re the person explaining to your cofounder why a package install beaconed to a random domain.&lt;/p&gt;

&lt;p&gt;Default-deny egress works because it doesn’t care &lt;em&gt;why&lt;/em&gt; the agent is misbehaving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt injection&lt;/li&gt;
&lt;li&gt;malicious dependency&lt;/li&gt;
&lt;li&gt;accidental command&lt;/li&gt;
&lt;li&gt;model bug&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it can’t phone home, it can’t exfiltrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical egress model for small teams
&lt;/h3&gt;

&lt;p&gt;You don’t need a service mesh or a policy engine to get 80% of the value.&lt;/p&gt;

&lt;p&gt;On a single host running disposable VMs, the pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give each VM a dedicated network interface (a &lt;code&gt;tap&lt;/code&gt; device, typically attached to a bridge).&lt;/li&gt;
&lt;li&gt;Apply firewall rules on that interface (&lt;code&gt;nftables&lt;/code&gt; on Linux, &lt;code&gt;pf&lt;/code&gt; on macOS, &lt;code&gt;iptables&lt;/code&gt; if you’re stuck in the past).&lt;/li&gt;
&lt;li&gt;Start with &lt;strong&gt;deny all&lt;/strong&gt;, then add an allowlist.&lt;/li&gt;
&lt;li&gt;Log denies. Deny logs are your best “what did the agent try to do?” signal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A concrete implementation hint: I’ve seen small teams bind VM traffic to a dedicated bridge (e.g., &lt;code&gt;br-agent&lt;/code&gt;) and apply egress rules only on that bridge. That way you’re not playing whack-a-mole with the host’s global networking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to allowlist (and how not to get tricked)
&lt;/h3&gt;

&lt;p&gt;Most coding agents need less network than people assume. Typical allowlist buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git hosting:&lt;/strong&gt; &lt;code&gt;github.com&lt;/code&gt; (and your internal Git host)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package registries:&lt;/strong&gt; &lt;code&gt;pypi.org&lt;/code&gt;, &lt;code&gt;files.pythonhosted.org&lt;/code&gt;, &lt;code&gt;registry.npmjs.org&lt;/code&gt;, distro mirrors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container registries:&lt;/strong&gt; your specific registry domains if you build images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time + identity:&lt;/strong&gt; an NTP source, your IdP endpoints if you’re doing OIDC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trap: DNS is an exfil channel. If you allow arbitrary DNS to arbitrary resolvers, you’re letting the agent encode secrets in queries. Treat DNS as part of egress.&lt;/p&gt;

&lt;p&gt;The safer approach for a small setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a single resolver you control (even a local caching resolver).&lt;/li&gt;
&lt;li&gt;Pin or restrict DNS egress to that resolver.&lt;/li&gt;
&lt;li&gt;Allowlist by &lt;strong&gt;domain + resolved IP ranges&lt;/strong&gt;, not “anything on 443.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the short version: &lt;strong&gt;allowlist destinations, not ports&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disposable filesystems: snapshot, run, roll back
&lt;/h2&gt;

&lt;p&gt;Agents are messy. They create files. They install packages. They “just try something.” That’s the whole value prop.&lt;/p&gt;

&lt;p&gt;Your filesystem design should assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every run will leave garbage&lt;/li&gt;
&lt;li&gt;Some runs will try malware-style persistence&lt;/li&gt;
&lt;li&gt;You’ll eventually need to answer “what changed?”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The base image + overlay pattern
&lt;/h3&gt;

&lt;p&gt;The simplest durable design is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;read-only base image&lt;/strong&gt; you patch and update deliberately (weekly is fine).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;copy-on-write overlay&lt;/strong&gt; (per run) that captures all changes.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;scratch/work volume&lt;/strong&gt; (optional) for larger temp files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the VM side, the concept maps cleanly to qcow2 backing files or overlayfs-like semantics depending on your stack. The important part is operational: every run starts from the same base, and the overlay dies with the run.&lt;/p&gt;

&lt;p&gt;A number to keep you honest: if you run 20 agent tasks a day and each leaves behind 2 GB of junk, you’re burning &lt;strong&gt;40 GB/day&lt;/strong&gt;. Disposable overlays make cleanup deterministic.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should be persisted vs discarded after each agent run?
&lt;/h3&gt;

&lt;p&gt;Persisting the wrong stuff is how sandboxes quietly become “semi-trusted pet environments.” That’s when weird, unreproducible issues show up. Then people blame the model. It was the state.&lt;/p&gt;

&lt;p&gt;My rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persist:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Build artifacts you intentionally export (binaries, patches, generated docs)&lt;/li&gt;
&lt;li&gt;A structured audit bundle (more on that below)&lt;/li&gt;
&lt;li&gt;A minimal “run manifest” (inputs, tool permissions, allowlist config, VM image hash)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discard:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The VM disk overlay&lt;/li&gt;
&lt;li&gt;Package caches (&lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;apt&lt;/code&gt;) unless you can isolate them safely&lt;/li&gt;
&lt;li&gt;Shell history inside the VM (you already have external transcripts)&lt;/li&gt;
&lt;li&gt;Any copied workspace that contains secrets&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Caching is the one everyone tries to sneak back in for speed. If you want warm performance without persistent state, use a &lt;strong&gt;warm pool&lt;/strong&gt; of pre-booted VMs with empty overlays, not long-lived disks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets injection without leaving landmines
&lt;/h2&gt;

&lt;p&gt;If your agent can access production credentials, you’ve built a very expensive secret-leaking machine.&lt;/p&gt;

&lt;p&gt;The goal is not “the agent can deploy.” The goal is “the agent can deploy &lt;em&gt;in a narrow, revocable way&lt;/em&gt;.”&lt;/p&gt;

&lt;h3&gt;
  
  
  The least-bad secrets lifecycle
&lt;/h3&gt;

&lt;p&gt;For small teams, this pattern holds up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mint a short-lived token per run (minutes, not days).&lt;/li&gt;
&lt;li&gt;Scope it to the minimum set of actions (read-only if you can).&lt;/li&gt;
&lt;li&gt;Inject it at boot using a user-data style mechanism (cloud-init-like), or a one-shot secrets file mounted in memory.&lt;/li&gt;
&lt;li&gt;Redact secrets in logs at the boundary (tool runner) before anything gets shipped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concrete examples of “scoped to minimum”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Git token that can only read a single repo.&lt;/li&gt;
&lt;li&gt;A package registry token that can only download, not publish.&lt;/li&gt;
&lt;li&gt;A cloud token that can only write to one bucket prefix for artifacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And please stop putting long-lived secrets in environment variables if you can avoid it. Processes leak env. Debug logs leak env. People paste env into tickets.&lt;/p&gt;

&lt;p&gt;If you’re doing &lt;a href="https://dev.to/blog/evaluate-ai-agents-production"&gt;AI in production&lt;/a&gt; work, secrets hygiene is where “prototype” turns into “adult supervision.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditability: make every run reviewable after the fact
&lt;/h2&gt;

&lt;p&gt;A sandbox that can’t be audited is security theater.&lt;/p&gt;

&lt;p&gt;Assume you will eventually need to answer these four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What commands did the agent run?&lt;/li&gt;
&lt;li&gt;What network destinations did it try to reach?&lt;/li&gt;
&lt;li&gt;What files did it change?&lt;/li&gt;
&lt;li&gt;What artifacts did it produce?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can’t answer those quickly, you don’t have control. You have vibes.&lt;/p&gt;

&lt;h3&gt;
  
  
  “Wrap tool entrypoints” means one choke point
&lt;/h3&gt;

&lt;p&gt;Instead of letting the agent call &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, and &lt;code&gt;curl&lt;/code&gt; directly, route everything through a single “tool runner” entrypoint. This is where you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;log argv + working directory&lt;/li&gt;
&lt;li&gt;capture stdout/stderr&lt;/li&gt;
&lt;li&gt;record exit code + runtime duration&lt;/li&gt;
&lt;li&gt;attach a permission context (“read-only repo”, “network allowlist v3”, “no write outside /workspace”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can structure logs as OpenTelemetry spans if you want to get fancy. I wrote a full schema for this in &lt;a href="https://dev.to/blog/ai-agent-observability-logging-schema"&gt;AI agents&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to log (minimum viable forensics)
&lt;/h3&gt;

&lt;p&gt;Per run, I want a bundle that contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run manifest:&lt;/strong&gt; timestamp, VM image hash, agent version, tool policy version, egress allowlist version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command transcript:&lt;/strong&gt; every tool call, args, cwd, exit code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem diff summary:&lt;/strong&gt; list of files created/modified/deleted under &lt;code&gt;/workspace&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network flow log:&lt;/strong&gt; destination IP:port, SNI/hostname if available, bytes sent/received, allow/deny decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifacts:&lt;/strong&gt; patch files, build outputs, test reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s enough to reconstruct intent without saving the entire VM disk.&lt;/p&gt;

&lt;p&gt;A concrete retention guideline that won’t bankrupt you: keep audit bundles for &lt;strong&gt;30 days&lt;/strong&gt; by default, and keep “suspicious runs” for &lt;strong&gt;180 days&lt;/strong&gt;. If you don’t have a security team, your future self is the security team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started: a no-Kubernetes architecture that actually works
&lt;/h2&gt;

&lt;p&gt;Here’s the prescriptive design I’d ship for a solo dev or a small team on a single dev server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture: one host, one orchestrator, many disposable VMs
&lt;/h3&gt;

&lt;p&gt;Components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent orchestrator&lt;/strong&gt; (a small service or even a CLI) that:

&lt;ul&gt;
&lt;li&gt;creates a VM per run&lt;/li&gt;
&lt;li&gt;attaches overlay disks&lt;/li&gt;
&lt;li&gt;configures networking&lt;/li&gt;
&lt;li&gt;injects secrets&lt;/li&gt;
&lt;li&gt;starts the agent tool loop&lt;/li&gt;
&lt;li&gt;exports artifacts + logs&lt;/li&gt;
&lt;li&gt;destroys the VM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MicroVM runtime:&lt;/strong&gt; Firecracker if you’re on Linux and want density; otherwise a standard VM stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer VM wrapper (laptop ergonomics):&lt;/strong&gt; &lt;a href="https://github.com/lima-vm/lima/blob/master/README.md" rel="noopener noreferrer"&gt;Lima&lt;/a&gt; is a pragmatic choice on macOS/Linux because it launches Linux VMs with automatic file sharing and port forwarding (similar to WSL2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Firecracker’s own description is clear: it’s purpose-built for “secure, multi-tenant container and function-based services,” implemented as a KVM-based VMM with a minimal device model to reduce attack surface. That’s exactly the shape we want for “agent runs arbitrary tool code.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Warm pools without Kubernetes
&lt;/h3&gt;

&lt;p&gt;Competitor posts love warm pools implemented with CRDs. You don’t need that.&lt;/p&gt;

&lt;p&gt;A warm pool for small teams is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;strong&gt;N pre-booted VMs&lt;/strong&gt; paused/idle (N is usually 2–10).&lt;/li&gt;
&lt;li&gt;Each VM is sitting on the same base image but with an empty overlay.&lt;/li&gt;
&lt;li&gt;When a run starts, you assign it a warm VM, attach a fresh overlay, apply policy, and go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a hard concurrency limit (start with &lt;strong&gt;2&lt;/strong&gt; if you’re on a laptop)&lt;/li&gt;
&lt;li&gt;CPU/memory caps per VM (e.g., &lt;strong&gt;2 vCPU&lt;/strong&gt;, &lt;strong&gt;4–8 GB RAM&lt;/strong&gt; per run)&lt;/li&gt;
&lt;li&gt;a wall-clock timeout per run (e.g., &lt;strong&gt;10–20 minutes&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is less about cost and more about blast radius. Unlimited concurrency is how an agent turns a small bug into a host meltdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  When you actually should use Kubernetes
&lt;/h3&gt;

&lt;p&gt;If you’re already operating Kubernetes well, it can help with scheduling, packaging, and lifecycle. The industry trend is real. The Kubernetes SIGs project &lt;a href="https://github.com/kubernetes-sigs/agent-sandbox" rel="noopener noreferrer"&gt;agent-sandbox&lt;/a&gt; literally describes itself as enabling management of “isolated, stateful, singleton workloads” for “AI agent runtimes.”&lt;/p&gt;

&lt;p&gt;But K8s doesn’t remove the need for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;thoughtful default-deny egress&lt;/li&gt;
&lt;li&gt;secrets scoping&lt;/li&gt;
&lt;li&gt;auditable tool boundaries&lt;/li&gt;
&lt;li&gt;snapshot rollback patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t have those, you just have a compromised agent… scheduled nicely.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Inline illustration suggestion: “Single-host” architecture diagram with: base image store, overlay store, egress firewall, secrets broker, artifact store, log store.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A reality check (because nothing is perfect)
&lt;/h2&gt;

&lt;p&gt;This approach isn’t free. It’s just the best trade I’ve found for the “agents with real tools” era.&lt;/p&gt;

&lt;p&gt;Here are the honest limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You’re still trusting the host.&lt;/strong&gt; VM isolation reduces guest-to-host breakout risk, but it doesn’t eliminate it. Patch your kernel. Use hardware virtualization. Reduce host attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress allowlists are operational work.&lt;/strong&gt; Registries change IPs. CDNs are annoying. If you allowlist too broadly, you lose the point. If you allowlist too narrowly, your agent can’t do its job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs can leak secrets.&lt;/strong&gt; If you don’t redact at the boundary, you’ll end up storing credentials in logs. That’s worse than not logging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance and UX tradeoffs are real.&lt;/strong&gt; Starting a VM, attaching disks, applying firewall rules. It’s extra latency. Firecracker’s design exists because people wanted VM boundaries without VM pain, but there’s still overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more: if your agent needs to interact with a user’s real browser session or OS GUI, a Linux VM sandbox helps, but it doesn’t solve the “human session is the crown jewels” problem. That’s a different architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  A pragmatic posture for 2026
&lt;/h3&gt;

&lt;p&gt;My bias is that more teams will ship agents with tool access before they ship proper security controls. The market rewards speed. Incidents punish you later.&lt;/p&gt;

&lt;p&gt;Running this blog’s 7-agent publishing pipeline (261+ posts), I’ve learned that &lt;strong&gt;deterministic gates catch an entire class of failures that “just use a smarter model” will never reliably catch&lt;/strong&gt;. Sandboxing is the same kind of boring engineering. It’s not about smarter agents. It’s about guardrails that don’t get confused.&lt;/p&gt;

&lt;p&gt;If you want adjacent reading on operationalizing agent systems, start with &lt;a href="https://dev.to/blog/ai-agent-control-flow-patterns"&gt;agent orchestration&lt;/a&gt;, &lt;a href="https://dev.to/blog/ai-security-complete-guide"&gt;AI security&lt;/a&gt;, and &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;AI in production&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point nobody wants to say out loud
&lt;/h2&gt;

&lt;p&gt;Most “agent safety” conversations are still stuck on model behavior. That’s the wrong layer.&lt;/p&gt;

&lt;p&gt;Tool-using agents are systems. Systems fail. Systems get attacked. And when they do, the only thing that matters is blast radius.&lt;/p&gt;

&lt;p&gt;My prediction: by the time we hit 2027, “agent runs tools on the host” will be viewed the same way we now view “production app runs as root.” It’ll still exist, but it’ll be a red flag.&lt;/p&gt;

&lt;p&gt;If you’re building agents today, you have a chance to make disposable Linux VM sandboxes the default. Not because it’s trendy. Because it’s the first design that lets you sleep.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/ai-agent-sandbox-linux-vm?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ai-agent-sandbox-linux-vm" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sandboxing</category>
      <category>aiagents</category>
      <category>linux</category>
      <category>aisecurity</category>
    </item>
    <item>
      <title>Deepfake Voice Detection: 7-Step Detector Eval Guide [2026]</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:48:49 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/deepfake-voice-detection-7-step-detector-eval-guide-2026-4e0d</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/deepfake-voice-detection-7-step-detector-eval-guide-2026-4e0d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/deepfake-voice-detection-evaluation" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deepfake voice detection is the practice of determining whether an audio clip or live call contains human speech or AI-generated / voice-cloned speech, using a mix of machine learning signals, provenance metadata, and operational controls.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deepfake voice detection is an evaluation problem before it’s a tooling problem. If you can’t describe your threat model and test set, you can’t buy your way out.&lt;/li&gt;
&lt;li&gt;Your detector bake-off should include telephony (8 kHz), re-recording, codec damage, noise, and overlapped speech. These break “99% accurate” demos fast.&lt;/li&gt;
&lt;li&gt;Report more than accuracy. At minimum, publish AUC, EER, calibration (ECE or Brier), and latency at your target throughput.&lt;/li&gt;
&lt;li&gt;Choose thresholds based on fraud cost, not vibes. False positives burn call center capacity. False negatives burn money.&lt;/li&gt;
&lt;li&gt;In production, detection is defense-in-depth: combine detectors with out-of-band verification and provenance standards like C2PA.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If a vendor can’t tell you what breaks their detector, you’re not buying security. You’re buying marketing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’m writing this as a “part 2” to my tools-focused post on &lt;a href="https://dev.to/blog/deepfake-voice-detection-tools-tested"&gt;deepfake voice detection&lt;/a&gt;. That post is doing fine, but Google is pretty clearly asking for something else on the main query: a repeatable, defensible evaluation methodology that a security or fraud team can actually run.&lt;/p&gt;

&lt;p&gt;Also, this is not theoretical. Per my own Google Search Console snapshot for this site, the exact query &lt;strong&gt;“deepfake voice detection”&lt;/strong&gt; has been hovering around &lt;strong&gt;average position ~11&lt;/strong&gt; over the last ~90 days. That is page-2 purgatory. The way out is not “more tools”. It’s a scorecard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an AI Voice Detector and Why Does It Matter in 2026?
&lt;/h2&gt;

&lt;p&gt;An AI voice detector is a system that analyzes audio and outputs a likelihood score (or label) that the speech was generated or converted by a model rather than spoken by a human.&lt;/p&gt;

&lt;p&gt;Why it matters in 2026 is simple: voice is now an interface for money. Call centers, banks, crypto exchanges, and even internal IT helpdesks treat “a human voice on the phone” as a weak-but-useful authentication factor. Voice cloning made that assumption dangerous.&lt;/p&gt;

&lt;p&gt;Here’s the stance I’ll defend through the rest of this post:&lt;/p&gt;

&lt;p&gt;Detection alone will not “solve” voice fraud. But &lt;strong&gt;evaluated detection&lt;/strong&gt; can move you from blind to measurable. And measurable is how you get budget, reduce loss, and avoid vendor theater.&lt;/p&gt;

&lt;p&gt;Two practical consequences you should internalize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your accuracy number is meaningless without your audio pipeline.&lt;/strong&gt; A detector that looks great on clean WAV files can fall apart after one Opus transcode and a noisy office.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your success metric isn’t “catch deepfakes”.&lt;/strong&gt; It’s something like “reduce successful impersonation losses by 30% without increasing handle time by 20 seconds.” That forces you to think thresholds, escalation, and fallbacks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you’re building fraud defenses, think of this like spam detection circa 2004. The model is part of the system. The system is the product.&lt;/p&gt;

&lt;p&gt;(Visual break: architecture diagram of a call flow with scoring + escalation.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rising Threat of AI Voice Fraud
&lt;/h2&gt;

&lt;p&gt;The “AI voice fraud” story is already boring in the worst way. Not because it’s solved. Because it’s becoming routine.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.fbi.gov/how-we-can-help-you/scams-and-safety/common-scams-and-crimes/artificial-intelligence-scams" rel="noopener noreferrer"&gt;Federal Bureau of Investigation&lt;/a&gt; has a public warning page specifically about criminals using AI for scams, including &lt;strong&gt;AI-generated audio&lt;/strong&gt; for impersonation. Their mitigations aren’t “use a better detector”. They’re operational: callback numbers, code words, and verification steps.&lt;/p&gt;

&lt;p&gt;That’s a huge tell.&lt;/p&gt;

&lt;p&gt;Law enforcement is implicitly acknowledging what most detector vendors don’t like to say out loud: &lt;strong&gt;deepfake voice detection is probabilistic, and fraud is adaptive.&lt;/strong&gt; So you need layered defenses.&lt;/p&gt;

&lt;p&gt;In practice, I see three threat patterns show up repeatedly in incident postmortems across the industry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Family emergency / executive urgency scripts.&lt;/strong&gt; The audio doesn’t need to be perfect. It just needs to create panic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call center account takeover.&lt;/strong&gt; Fraudsters target the lowest-friction lane: “reset my password”, “change my payout details”, “update my phone number.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal helpdesk / IT social engineering.&lt;/strong&gt; If your org has SSO, the helpdesk is a crown-jewel path. A good clone plus one leaked employee detail is a bad day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run a CX org, a bank, or even a SaaS with high-value accounts, the right mindset is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assume you will hear AI-generated speech in inbound calls.&lt;/li&gt;
&lt;li&gt;Assume attackers will iterate after the first time you block them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which brings us to the part most content skips: how detection works enough to evaluate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Voice Detection Technology Works
&lt;/h2&gt;

&lt;p&gt;Most “how it works” explanations stop at “spectrograms” and call it a day. That’s not enough for evaluation.&lt;/p&gt;

&lt;p&gt;A useful mental model is that detectors typically combine a few families of signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spectral / feature-space artifacts&lt;/strong&gt;: classic audio features (MFCC-like representations, spectral bands) plus learned features that pick up inconsistencies in how synthetic audio distributes energy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neural codec fingerprints&lt;/strong&gt;: a lot of modern TTS/voice conversion pipelines pass through neural codecs. That can leave subtle, model-family-specific traces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prosody and temporal cues&lt;/strong&gt;: timing, stress, rhythm, breath patterns, turn-taking. Humans are messy. Models are getting better, but they’re still “too consistent” in weird ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model-specific classifiers&lt;/strong&gt;: vendor detectors that identify audio generated by that vendor (or their model families). These can be strong for narrow provenance detection and weak for general “is this fake?” detection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the practical implication for evaluation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a detector is primarily trained on a specific generation stack, it may be brittle against unseen stacks.&lt;/li&gt;
&lt;li&gt;If it relies heavily on clean spectral cues, it may fail under compression or re-recording.&lt;/li&gt;
&lt;li&gt;If it claims “general deepfake detection,” you should force it to prove generalization across datasets and perturbations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why I’m skeptical of one-number claims. A detector that’s “99% accurate” on dataset A might be “coin flip” on dataset B. And you’ll only discover that after you ship it, unless you build a real eval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best AI Voice Detector Tools in 2026 (Free and Paid)
&lt;/h2&gt;

&lt;p&gt;You can find long lists of tools. I already wrote one with actual comparisons in &lt;a href="https://dev.to/blog/ai-voice-detector-detect-audio"&gt;AI voice detector: tools tested&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this post, the key is: tools are only “best” relative to your threat model and operating constraints. Still, you should understand the common categories you’ll be evaluating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vendor provenance classifiers&lt;/strong&gt;: for example, the &lt;a href="https://elevenlabs.io/ai-speech-classifier" rel="noopener noreferrer"&gt;ElevenLabs AI Speech Classifier&lt;/a&gt; returns a probability that audio was generated with ElevenLabs tech. On their own page they explicitly warn: it &lt;strong&gt;“Does not reliably classify audio generated with the Eleven v3 model.”&lt;/strong&gt; That’s not a knock. It’s an honest statement that should shape your expectations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;General-purpose deepfake detectors&lt;/strong&gt;: typically research-derived models or commercial services trained across multiple TTS/VC methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call-center / telephony-integrated solutions&lt;/strong&gt;: detectors plus VoIP integration, streaming, dashboards, and analyst workflows. See my deployment-focused write-up on &lt;a href="https://dev.to/blog/deepfake-voice-detection-call-centers"&gt;deepfake voice detection&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DIY / open source research models&lt;/strong&gt;: flexible, but you own the eval, the latency, and the failure modes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free vs paid in practice often comes down to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency and throughput guarantees&lt;/strong&gt; (paid vendors usually win)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API and integrations&lt;/strong&gt; (paid wins)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparency into training data and failure cases&lt;/strong&gt; (often open models win)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost predictability at scale&lt;/strong&gt; (varies; do the math)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re running a bake-off, shortlist 2–4 detectors from different categories. If all your candidates are the same kind of model, you’re not doing evaluation. You’re doing brand comparison.&lt;/p&gt;

&lt;p&gt;(Visual break: “detector types” illustration.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How Accurate Are AI Voice Detectors?
&lt;/h2&gt;

&lt;p&gt;This question is always asked and almost always answered badly.&lt;/p&gt;

&lt;p&gt;“How accurate are deepfake voice detectors?” is like asking “how accurate are spam filters?” Accurate on what emails? For what cost of false positives? With what adversary?&lt;/p&gt;

&lt;p&gt;So I’ll define what “accurate” should mean in a vendor eval:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discrimination&lt;/strong&gt;: can the model separate real vs fake across a range of thresholds? (AUC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operating point performance&lt;/strong&gt;: at your chosen threshold, what are false positives and false negatives? (confusion matrix)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-weighted performance&lt;/strong&gt;: what happens when a false negative costs $10,000 and a false positive costs $15 in handle time? (minDCF / custom cost metric)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration&lt;/strong&gt;: does “0.9 probability” actually mean “~90% of these are fake” over time? (ECE or Brier)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robustness&lt;/strong&gt;: does it still work after transformations your audio pipeline will apply?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: can you run it fast enough to be useful in-call?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a compact table you can drop into an internal scorecard.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;th&gt;Why you should care in production&lt;/th&gt;
&lt;th&gt;Common way it gets gamed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AUC (ROC-AUC)&lt;/td&gt;
&lt;td&gt;Overall separability across thresholds&lt;/td&gt;
&lt;td&gt;Good for comparing models before choosing a threshold&lt;/td&gt;
&lt;td&gt;Evaluated on clean lab audio only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EER&lt;/td&gt;
&lt;td&gt;Threshold where false accept = false reject&lt;/td&gt;
&lt;td&gt;Useful sanity check for “balanced” performance&lt;/td&gt;
&lt;td&gt;Hides asymmetry of real fraud costs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calibration (ECE/Brier)&lt;/td&gt;
&lt;td&gt;Whether scores map to real probabilities&lt;/td&gt;
&lt;td&gt;Critical if you do risk-based routing&lt;/td&gt;
&lt;td&gt;Vendors output “confidence” that isn’t calibrated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost-weighted metric (minDCF-like)&lt;/td&gt;
&lt;td&gt;Performance under your loss function&lt;/td&gt;
&lt;td&gt;Aligns model choice to business impact&lt;/td&gt;
&lt;td&gt;Vendors pick a loss function that flatters them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency (ms)&lt;/td&gt;
&lt;td&gt;Time to score per chunk / call&lt;/td&gt;
&lt;td&gt;Determines streaming feasibility&lt;/td&gt;
&lt;td&gt;Measured on tiny batches, not real throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robustness suite pass rate (%)&lt;/td&gt;
&lt;td&gt;How performance degrades under transforms&lt;/td&gt;
&lt;td&gt;Predicts in-the-wild failure&lt;/td&gt;
&lt;td&gt;Not reported at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A concrete number you can use as a forcing function: if your call center can tolerate &lt;strong&gt;≤ 250 ms&lt;/strong&gt; added per 2-second audio chunk for streaming risk scoring, that’s your hard budget. Any detector that can’t hit it is not a “real-time detector.” It’s a post-call analytics tool.&lt;/p&gt;

&lt;p&gt;If you want a model for how to build evals in general (not just audio), I’ve written extensively about regression gating in &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;AI engineering evals&lt;/a&gt; and production monitoring in &lt;a href="https://dev.to/blog/evaluate-ai-agents-production"&gt;evaluate AI agents in production&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can AI Voice Detection Be Fooled?
&lt;/h2&gt;

&lt;p&gt;Yes. And the only interesting question is: &lt;strong&gt;which attacks matter in your production environment?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most detector demos are evaluated on pristine inputs. Fraud doesn’t happen on pristine inputs. It happens through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;phone mics&lt;/li&gt;
&lt;li&gt;speakerphones&lt;/li&gt;
&lt;li&gt;call recording systems&lt;/li&gt;
&lt;li&gt;conferencing apps&lt;/li&gt;
&lt;li&gt;VoIP codecs&lt;/li&gt;
&lt;li&gt;background noise&lt;/li&gt;
&lt;li&gt;angry people talking over each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So here’s the adversarial test matrix I actually care about. If your vendor can’t run this, you should run it yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The “real call flow” transformation suite
&lt;/h3&gt;

&lt;p&gt;At minimum, test these transformations on both real and fake audio:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Telephony bandlimiting&lt;/strong&gt;: downsample / band-limit to &lt;strong&gt;8 kHz&lt;/strong&gt; (PSTN-like). This is the most common “silent killer.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codec transcoding&lt;/strong&gt;: run through Opus and AAC at a few bitrates. Real systems re-encode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-recording&lt;/strong&gt;: play audio through a cheap speaker and capture it with a phone mic. This destroys many fingerprint-style cues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additive noise&lt;/strong&gt;: office noise, street noise, café noise at multiple SNRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packet loss / jitter simulation&lt;/strong&gt;: if you do VoIP streaming, simulate dropped frames and jitter buffers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-stretch / pitch shift (mild)&lt;/strong&gt;: not sci-fi. These happen accidentally in some pipelines, and adversaries can do them intentionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlapped speech&lt;/strong&gt;: agent interrupts customer, customer talks over agent. Many detectors implicitly assume one speaker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each transformation, don’t just record “accuracy”. Record &lt;strong&gt;delta&lt;/strong&gt; versus clean audio. A robust detector has graceful degradation. A brittle detector cliff-dives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hard negatives: the stuff that triggers false positives
&lt;/h3&gt;

&lt;p&gt;If you deploy detection, your biggest operational pain is false positives. So you need “hard negative” audio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;strong accents&lt;/li&gt;
&lt;li&gt;emotional speech (crying, yelling)&lt;/li&gt;
&lt;li&gt;whispered speech&lt;/li&gt;
&lt;li&gt;low-quality mics&lt;/li&gt;
&lt;li&gt;people with speech impairments&lt;/li&gt;
&lt;li&gt;background music (retail stores are a nightmare)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m being blunt here: many teams only test on “nice narrator voice in a quiet room.” That’s how you ship a detector that flags half of your customers in Toronto because they’re code-switching mid-sentence.&lt;/p&gt;

&lt;p&gt;And yes, attackers can actively try to fool detectors. They can re-record, add noise, or choose generation tools that are less detectable. That’s why you treat this like any other adversarial classification system: you don’t ship it once. You monitor it.&lt;/p&gt;

&lt;p&gt;If you’ve done any &lt;a href="https://dev.to/blog/ai-security-complete-guide"&gt;AI security&lt;/a&gt; work, you’ll recognize the pattern: threat model, red team, telemetry, iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evaluation Protocol: A 7-Step Detector Scorecard You Can Reuse
&lt;/h2&gt;

&lt;p&gt;This is the core of the post. It’s the part I wanted to exist when I started evaluating audio detectors for real systems.&lt;/p&gt;

&lt;p&gt;I’ll lay it out as a protocol you can run in a week or two.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Write the threat model in one page
&lt;/h3&gt;

&lt;p&gt;If you skip this, you will measure the wrong thing.&lt;/p&gt;

&lt;p&gt;Your one-pager should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Channel&lt;/strong&gt;: inbound call center (PSTN/VoIP), voice notes, conferencing, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary capability&lt;/strong&gt;: commodity voice clone tools vs targeted high-effort clones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: social engineering for payout change, password reset, or reputation harm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints&lt;/strong&gt;: latency budget, privacy constraints, storage limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make it painfully specific. “AI fraud” is not a threat model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Build the dataset plan (lab + in-the-wild)
&lt;/h3&gt;

&lt;p&gt;Use a mix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open benchmark datasets&lt;/strong&gt; for reproducibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own in-the-wild samples&lt;/strong&gt; for reality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For open benchmarks, the industry anchor is the &lt;a href="https://www.asvspoof.org/" rel="noopener noreferrer"&gt;ASVspoof Challenge&lt;/a&gt;. It’s the de-facto hub for spoofing countermeasures for automatic speaker verification, and it provides shared evaluation plans and datasets across multiple editions.&lt;/p&gt;

&lt;p&gt;ASVspoof has multiple task flavors over the years (logical access vs physical access vs deepfake). The exact split you use matters less than the discipline: consistent protocols, held-out evaluation sets, and reproducible reporting.&lt;/p&gt;

&lt;p&gt;Your internal dataset should include at least &lt;strong&gt;200–500&lt;/strong&gt; real calls (properly consented and handled) across your key customer segments. If you can’t collect that, you’re not ready to operationalize. You’ll be flying blind on false positives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Define the transformation suite and generate variants
&lt;/h3&gt;

&lt;p&gt;Take every clip in your evaluation set and generate transformed variants using the matrix above.&lt;/p&gt;

&lt;p&gt;Rule of thumb: aim for &lt;strong&gt;10–20 variants per source clip&lt;/strong&gt;. That sounds expensive until you realize it’s mostly automation.&lt;/p&gt;

&lt;p&gt;Now you have an evaluation set that matches your pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Choose metrics you will publish internally
&lt;/h3&gt;

&lt;p&gt;Pick a minimal set and stick to it across vendors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ROC-AUC&lt;/li&gt;
&lt;li&gt;EER&lt;/li&gt;
&lt;li&gt;Calibration: ECE or Brier&lt;/li&gt;
&lt;li&gt;Latency: p50/p95 per chunk (or per file)&lt;/li&gt;
&lt;li&gt;Cost-weighted score: your loss function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a template for how to treat evals as a regression gate, borrow the approach from &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;AI engineering evals&lt;/a&gt; and apply it to audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Calibrate scores and pick thresholds based on cost
&lt;/h3&gt;

&lt;p&gt;This is where most teams get lazy and then blame the model.&lt;/p&gt;

&lt;p&gt;You should pick an operating threshold using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;estimated fraud loss per successful event (e.g., &lt;strong&gt;$5,000&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;estimated analyst / handle-time cost per false positive (e.g., &lt;strong&gt;$10–$50&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;your acceptable friction budget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your fraud loss is 100x higher than your false positive cost, you should bias toward catching more even at the cost of more reviews. If it’s the opposite (high-volume, low-loss), you bias toward fewer false positives.&lt;/p&gt;

&lt;p&gt;Write the numbers down. Make someone sign off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Run a red-team day
&lt;/h3&gt;

&lt;p&gt;Treat this like a practical exercise, not a research paper.&lt;/p&gt;

&lt;p&gt;Have a small group generate attacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;re-recorded audio&lt;/li&gt;
&lt;li&gt;multilingual samples&lt;/li&gt;
&lt;li&gt;code-switching&lt;/li&gt;
&lt;li&gt;overlapped speech&lt;/li&gt;
&lt;li&gt;different TTS/VC providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Log which ones slip through and why.&lt;/p&gt;

&lt;p&gt;This is where you learn if your detector is robust or if it’s memorizing one family of artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Produce a vendor scorecard (one page + appendix)
&lt;/h3&gt;

&lt;p&gt;Your final output should be something your procurement and security leadership can read.&lt;/p&gt;

&lt;p&gt;Include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dataset card (what’s in/out)&lt;/li&gt;
&lt;li&gt;transformation suite definition&lt;/li&gt;
&lt;li&gt;metrics table&lt;/li&gt;
&lt;li&gt;failure modes and examples&lt;/li&gt;
&lt;li&gt;recommended threshold(s) by lane (high-risk vs low-risk)&lt;/li&gt;
&lt;li&gt;operational plan (where it runs, what happens on “uncertain”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to formalize the “operational plan” piece, it maps well to the observability + governance work I’ve done for &lt;a href="https://dev.to/pillars/ai-agents"&gt;AI agents&lt;/a&gt; and &lt;a href="https://dev.to/pillars/ai-engineering-production"&gt;AI in production&lt;/a&gt;. Different domain, same discipline.&lt;/p&gt;

&lt;p&gt;(Visual break: scorecard template mockup.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Datasets, Multilingual Edge Cases, and Drift Monitoring (The Stuff That Actually Breaks You)
&lt;/h2&gt;

&lt;p&gt;Most posts never mention multilingual performance. That’s a mistake.&lt;/p&gt;

&lt;p&gt;Detectors are trained on distributions. Languages shift distributions. Accents shift distributions. Code-switching shifts distributions mid-utterance.&lt;/p&gt;

&lt;p&gt;If your customer base is multilingual (Canada says hi), you need to treat this as table stakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multilingual evaluation plan
&lt;/h3&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Include &lt;strong&gt;3+ languages&lt;/strong&gt; relevant to your users.&lt;/li&gt;
&lt;li&gt;Include accented speech within the same language (not just “US English”).&lt;/li&gt;
&lt;li&gt;Include code-switching samples (two languages in one call).&lt;/li&gt;
&lt;li&gt;Measure false positive rate per segment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A concrete operational target I like: your false positive rate should not increase by more than &lt;strong&gt;2x&lt;/strong&gt; for your top accent groups compared to your baseline. If it does, you’ll create an “AI detector” that is functionally an accent detector. That’s a reputational and legal hazard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift monitoring in production
&lt;/h3&gt;

&lt;p&gt;Deepfake voice detection is not set-and-forget.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a rolling holdout set of recent calls (consented, redacted)&lt;/li&gt;
&lt;li&gt;periodic re-scoring (weekly or monthly)&lt;/li&gt;
&lt;li&gt;alerting on distribution shifts in scores&lt;/li&gt;
&lt;li&gt;incident review when your false positive queue spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve built monitoring for probabilistic systems before, you’ll recognize this as the same problem as LLM output drift. My mental model comes from building deterministic gates and feedback loops in this site’s publishing pipeline (the boring, repeatable checks catch more than “bigger model review” ever did). The lesson carries: &lt;strong&gt;measure first, then automate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For logging and privacy patterns, borrow ideas from &lt;a href="https://dev.to/blog/ai-agent-observability-logging-schema"&gt;AI agent observability&lt;/a&gt; and adapt them for audio: store derived signals where possible, redact aggressively, keep retention short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio Watermarking and the C2PA Provenance Standard
&lt;/h2&gt;

&lt;p&gt;Detection answers: “does this look fake?”&lt;/p&gt;

&lt;p&gt;Provenance answers: “where did this come from, and what happened to it?”&lt;/p&gt;

&lt;p&gt;You want both.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://spec.c2pa.org/specifications/specifications/2.4/index.html" rel="noopener noreferrer"&gt;Coalition for Content Provenance and Authenticity (C2PA)&lt;/a&gt; publishes a technical specification for signing and verifying content provenance metadata. In plain English: content can carry cryptographic “content credentials” that tell you the origin and edit history, assuming the ecosystem adopts it and the metadata survives the journey.&lt;/p&gt;

&lt;p&gt;Watermarking research (like Meta’s AudioSeal project, referenced in the research brief) is the other side: embed a robust signal into generated audio so you can detect origin even after transformations.&lt;/p&gt;

&lt;p&gt;The hard reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provenance won’t be universal.&lt;/strong&gt; Attackers won’t cooperate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata can be stripped.&lt;/strong&gt; Many platforms re-encode and drop it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermarks are an arms race too.&lt;/strong&gt; They can degrade under heavy transforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the right production posture is defense-in-depth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If provenance exists and verifies, treat it as a strong signal.&lt;/li&gt;
&lt;li&gt;If provenance is missing, fall back to detection.&lt;/li&gt;
&lt;li&gt;If detection is uncertain, fall back to operational verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same pattern we use in &lt;a href="https://dev.to/blog/prompt-injection-2026-owasp-llm-vulnerability"&gt;LLM security&lt;/a&gt; and broader &lt;a href="https://dev.to/blog/ai-security-complete-guide"&gt;AI security&lt;/a&gt;: never bet the company on one probabilistic classifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should You Do If You Receive a Suspicious AI Voice Call?
&lt;/h2&gt;

&lt;p&gt;This is the part people actually need when they’re in the moment.&lt;/p&gt;

&lt;p&gt;If you receive a suspicious call (family emergency, bank request, executive request), here’s the playbook I’d want my own family to follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assume the voice can be faked.&lt;/strong&gt; Don’t argue about whether it “sounds real.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch channels.&lt;/strong&gt; Hang up and call back using a number you already trust (from contacts or official website).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a shared secret.&lt;/strong&gt; Families and teams should have a simple code word for emergencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow it down.&lt;/strong&gt; Scams rely on urgency. Create time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalate inside the org.&lt;/strong&gt; If it’s work-related, route to your fraud/security team with the recording if policy allows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice how none of this requires a detector. That’s intentional. Detectors are great for systems. Humans need habits.&lt;/p&gt;

&lt;p&gt;And yes, this aligns with the FBI’s emphasis on out-of-band verification for AI-enabled scams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases: Who Needs an AI Voice Detector?
&lt;/h2&gt;

&lt;p&gt;Not everyone needs deepfake voice detection. But if you match any of these, you should at least evaluate it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Banks / fintech / crypto exchanges&lt;/strong&gt;: account takeover and payout changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call centers&lt;/strong&gt;: password resets, address changes, high-value support lanes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprises with IT helpdesks&lt;/strong&gt;: social engineering into SSO resets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media and journalism&lt;/strong&gt;: verifying leaked audio, preventing reputational hits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketplaces&lt;/strong&gt;: seller/buyer disputes where voice evidence is used&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical heuristic: if a successful impersonation event can cost you &lt;strong&gt;$10k+&lt;/strong&gt; (or a headline), detector evaluation is worth the effort. If your worst-case loss is $50, spend your time on better authentication and agent training.&lt;/p&gt;

&lt;h2&gt;
  
  
  Legal Requirements: AI-Generated Audio Disclosure in 2026
&lt;/h2&gt;

&lt;p&gt;I’m not a lawyer, and you shouldn’t treat this as legal advice.&lt;/p&gt;

&lt;p&gt;But you should assume the regulatory direction is clear: &lt;strong&gt;disclosure and provenance expectations will increase&lt;/strong&gt; for AI-generated media, especially in advertising, political content, and consumer communications.&lt;/p&gt;

&lt;p&gt;Even if your jurisdiction doesn’t mandate disclosure for every AI-generated audio use case, your risk team should care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consent for call recording and analysis&lt;/li&gt;
&lt;li&gt;biometric / voiceprint handling policies&lt;/li&gt;
&lt;li&gt;retention limits&lt;/li&gt;
&lt;li&gt;explainability for adverse actions (e.g., blocking a customer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is another reason to invest in calibration, scorecards, and “what happens when uncertain.” Regulators don’t love black boxes that can’t justify decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line: ship an evaluation harness, not a detector
&lt;/h2&gt;

&lt;p&gt;Deepfake voice detection is going to follow the same trajectory as every other security classifier: the “model” becomes a commodity, and the competitive advantage shifts to evaluation, operations, and incident response.&lt;/p&gt;

&lt;p&gt;If you’re a buyer, my challenge is simple: stop asking vendors for a demo. Ask them for a &lt;strong&gt;failure-mode report&lt;/strong&gt; on 8 kHz telephony, re-recording, and multilingual audio.&lt;/p&gt;

&lt;p&gt;If you’re a builder, my prediction is even simpler: within &lt;strong&gt;12–18 months&lt;/strong&gt;, the teams that win will treat voice deepfake detection like they treat &lt;a href="https://dev.to/blog/rag-context-window-limitations"&gt;RAG&lt;/a&gt; and other probabilistic systems. Continuous evals. Drift monitoring. Escalation lanes. No hero metrics.&lt;/p&gt;

&lt;p&gt;The page-1 content for “deepfake voice detection” won’t be another listicle. It’ll be the first guide that gives security teams a protocol they can run, defend, and iterate. Build that, and the ranking will follow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/deepfake-voice-detection-evaluation?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=deepfake-voice-detection-evaluation" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deepfake</category>
      <category>voicesecurity</category>
      <category>fraud</category>
      <category>biometrics</category>
    </item>
    <item>
      <title>Debug HTTP/3 QUIC in Production: 8-Step Playbook [2026]</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Wed, 12 Aug 2026 00:43:02 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/debug-http3-quic-in-production-8-step-playbook-2026-1105</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/debug-http3-quic-in-production-8-step-playbook-2026-1105</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/debug-http3-quic-production" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;HTTP/3 debugging in production is the art of proving what protocol real users negotiated (H3 vs H2 vs H1), why QUIC handshakes fail, and whether enabling H3 actually improved latency.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You haven’t “enabled HTTP/3” until you can show negotiated &lt;code&gt;h3&lt;/code&gt; on real requests, not just an &lt;code&gt;Alt-Svc&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;The fastest Wireshark-free triage is: DevTools protocol column → &lt;code&gt;Alt-Svc&lt;/code&gt; presence/caching → UDP/443 reachability → curl transcript → CDN/origin QUIC logs.&lt;/li&gt;
&lt;li&gt;Silent fallback to HTTP/2 is normal behavior, not a bug. Treat it like a decision tree with observable checkpoints.&lt;/li&gt;
&lt;li&gt;Measure impact by forcing H3 vs forcing H2 in the same client, with fresh connections, and by separating handshake time from TTFB.&lt;/li&gt;
&lt;li&gt;If you can’t attach artifacts (NetLog JSON + curl &lt;code&gt;-v&lt;/code&gt; output + edge log fields) to the incident ticket, you’re not debugging. You’re guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you can’t produce a single artifact that says “this request used h3”, you don’t have an HTTP/3 problem. You have an observability problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On this blog I run a deterministic multi-step publishing pipeline with an incident log, and the lesson transfers cleanly to networking. Deterministic gates and reproducible artifacts beat vibes every time. The same mindset that saved me from shipping broken SEO templates at scale is the mindset that will save your on-call when QUIC is doing something weird at 2 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8-step HTTP/3 debugging checklist (copy/paste)
&lt;/h2&gt;

&lt;p&gt;This section is intentionally procedural. When you’re on-call, you don’t want lore. You want a checklist.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;In Chrome DevTools, confirm the negotiated protocol is actually &lt;code&gt;h3&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm &lt;code&gt;Alt-Svc&lt;/code&gt; is being advertised on the right responses&lt;/strong&gt; (and with a sane &lt;code&gt;ma&lt;/code&gt; value).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminate “connection reuse lies”&lt;/strong&gt; (fresh profile/incognito, disable cache, new connection).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate UDP/443 reachability from the failing network&lt;/strong&gt; (hotel Wi‑Fi, corp VPN, mobile carrier).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force HTTP/3 using curl and capture a verbose transcript&lt;/strong&gt; (&lt;code&gt;--http3&lt;/code&gt;, &lt;code&gt;-v&lt;/code&gt;, plus DNS/IPv6 notes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force HTTP/2 using curl and capture the same transcript&lt;/strong&gt; (so you can compare apples-to-apples).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull CDN edge logs that include protocol + QUIC handshake failure reasons&lt;/strong&gt; (or the closest available fields).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you own the origin QUIC stack, enable QUIC logs / qlog&lt;/strong&gt; long enough to capture a failing session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You’ll notice what’s missing: packet capture. Most devs can’t do it on the device/network where the bug happens anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm HTTP/3 is being used in Chrome (not just Alt-Svc)
&lt;/h2&gt;

&lt;p&gt;The most common failure mode I see is people stopping at “I see &lt;code&gt;Alt-Svc: h3=\":443\"&lt;/code&gt; so we’re on HTTP/3.” That’s not confirmation. That’s advertising.&lt;/p&gt;

&lt;p&gt;Here’s what I actually trust in Chrome:&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Add the Protocol column in DevTools&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open DevTools → Network.&lt;/li&gt;
&lt;li&gt;Right-click the header row (Name/Status/Type/…)&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Protocol&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Reload the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each request, you’ll see something like &lt;code&gt;h3&lt;/code&gt;, &lt;code&gt;h2&lt;/code&gt;, or &lt;code&gt;http/1.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;2) &lt;strong&gt;Filter to the requests that matter&lt;/strong&gt;&lt;br&gt;
The page might load some third-party assets over &lt;code&gt;h3&lt;/code&gt; while your HTML document is still on &lt;code&gt;h2&lt;/code&gt;. Filter for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The HTML document request&lt;/li&gt;
&lt;li&gt;Your critical JS bundle&lt;/li&gt;
&lt;li&gt;Your main API call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3) &lt;strong&gt;Don’t get tricked by connection reuse / coalescing&lt;/strong&gt;&lt;br&gt;
HTTP/3 is multiplexed, and browsers reuse connections aggressively.&lt;/p&gt;

&lt;p&gt;To avoid false confidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an Incognito window (fresh cache and connection pool).&lt;/li&gt;
&lt;li&gt;In DevTools Network, check &lt;strong&gt;Disable cache&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Hard reload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re trying to debug “first visit” behavior, this matters. &lt;code&gt;Alt-Svc&lt;/code&gt; can be cached, and the first navigation might be H2 even if subsequent navigations go H3.&lt;/p&gt;

&lt;p&gt;4) &lt;strong&gt;Look at response headers anyway&lt;/strong&gt;&lt;br&gt;
You still want to check &lt;code&gt;Alt-Svc&lt;/code&gt;, but interpret it correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is &lt;code&gt;Alt-Svc&lt;/code&gt; present on the HTML response (not just static assets)?&lt;/li&gt;
&lt;li&gt;Is it advertising the correct authority (hostname + port)?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;ma&lt;/code&gt; long enough to matter? (&lt;code&gt;ma=0&lt;/code&gt; is effectively “don’t cache this.”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A concrete number to keep in your head: &lt;code&gt;ma&lt;/code&gt; is in &lt;strong&gt;seconds&lt;/strong&gt;. So &lt;code&gt;ma=86400&lt;/code&gt; is one day.&lt;/p&gt;

&lt;p&gt;Visual breathing room: this is a great place to insert an image showing DevTools with the Protocol column.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify HTTP/3 with curl (and read the verbose output)
&lt;/h2&gt;

&lt;p&gt;Browsers are great, but they’re not reproducible enough for incident tickets. Curl is the “attachable artifact” tool.&lt;/p&gt;

&lt;p&gt;What you want from curl is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A transcript that shows which protocol was negotiated.&lt;/li&gt;
&lt;li&gt;A transcript you can re-run from a CI runner, a jump box, or a teammate’s laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The minimum curl commands I use
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Force HTTP/3:&lt;/strong&gt; &lt;code&gt;curl -v --http3 https://example.com/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force HTTP/2:&lt;/strong&gt; &lt;code&gt;curl -v --http2 https://example.com/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re testing an API endpoint, hit a specific URL, not &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to interpret curl output
&lt;/h3&gt;

&lt;p&gt;In verbose output, look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The resolved IP and whether it picked IPv4 or IPv6.&lt;/strong&gt; (This matters for UDP reachability and MTU issues.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ALPN negotiation.&lt;/strong&gt; For TLS over TCP you’ll often see &lt;code&gt;h2&lt;/code&gt;/&lt;code&gt;http/1.1&lt;/code&gt;. For QUIC, you want &lt;code&gt;h3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alt-Svc behavior.&lt;/strong&gt; Curl may print &lt;code&gt;Alt-Svc&lt;/code&gt; headers and it may cache them depending on build/options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;--http3&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;If it fails, run &lt;code&gt;--http2&lt;/code&gt; immediately after from the same machine/network.&lt;/li&gt;
&lt;li&gt;If H2 succeeds and H3 fails, you’ve narrowed the failure domain to UDP/QUIC/H3, not “site is down.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your curl doesn’t support HTTP/3, that’s also a signal. Not every environment has an ngtcp2/quiche-enabled build.&lt;/p&gt;

&lt;p&gt;Visual breathing room: this is a good place for an image of terminal output with the key lines highlighted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why HTTP/3 silently falls back to HTTP/2 (the real reasons)
&lt;/h2&gt;

&lt;p&gt;Silent fallback is a feature. HTTP/3 clients are supposed to recover.&lt;/p&gt;

&lt;p&gt;In production, fallback usually happens for boring reasons:&lt;/p&gt;

&lt;h3&gt;
  
  
  1) UDP/443 is blocked
&lt;/h3&gt;

&lt;p&gt;This is the big one. Enterprise firewalls, captive portals, and some VPNs still treat UDP like it’s suspicious.&lt;/p&gt;

&lt;p&gt;Symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser shows &lt;code&gt;h2&lt;/code&gt; even though &lt;code&gt;Alt-Svc&lt;/code&gt; is present.&lt;/li&gt;
&lt;li&gt;Curl &lt;code&gt;--http3&lt;/code&gt; hangs or errors quickly, while &lt;code&gt;--http2&lt;/code&gt; works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try the same URL on a different network: home Wi‑Fi vs phone hotspot.&lt;/li&gt;
&lt;li&gt;If hotspot works and corp Wi‑Fi doesn’t, stop blaming your CDN config.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Alt-Svc isn’t being sent where it needs to be
&lt;/h3&gt;

&lt;p&gt;Common misconfig:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Alt-Svc&lt;/code&gt; only on static assets, not the main HTML document.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Alt-Svc&lt;/code&gt; stripped by an intermediate proxy.&lt;/li&gt;
&lt;li&gt;Wrong port or authority advertised.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember: clients learn about H3 via &lt;code&gt;Alt-Svc&lt;/code&gt;. If you never advertise it on the “entry” response, you’ll never graduate clients to H3.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) QUIC version / ALPN mismatch
&lt;/h3&gt;

&lt;p&gt;Even if UDP is open, protocol negotiation can fail.&lt;/p&gt;

&lt;p&gt;Causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client and server don’t share a QUIC version.&lt;/li&gt;
&lt;li&gt;Server advertises &lt;code&gt;h3&lt;/code&gt; but the stack is misconfigured for the host/SNI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t need Wireshark to suspect this. You need server logs that say “handshake failed because …” (more on that below).&lt;/p&gt;

&lt;h3&gt;
  
  
  4) IPv6 weirdness and Happy Eyeballs outcomes
&lt;/h3&gt;

&lt;p&gt;Sometimes H3 works over IPv4 but not IPv6 (or vice versa). Browsers race connections. That can make your problem look “flaky”.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;If IPv6 path has a smaller effective MTU and drops fragments, QUIC’s initial handshake packets can get blackholed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5) MTU / fragmentation issues
&lt;/h3&gt;

&lt;p&gt;QUIC handshake packets can be larger than you expect.&lt;/p&gt;

&lt;p&gt;If a path drops IP fragments or ICMP “Packet Too Big” messages, you’ll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QUIC handshake failures on specific networks.&lt;/li&gt;
&lt;li&gt;Retries or timeouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where packet capture helps, but you can still debug it Wireshark-free by correlating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which networks fail&lt;/li&gt;
&lt;li&gt;Whether IPv6 fails more than IPv4&lt;/li&gt;
&lt;li&gt;Whether failure correlates with specific client OS versions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debug QUIC handshake failed (decision tree, fastest checks first)
&lt;/h2&gt;

&lt;p&gt;When someone pings you with “debug quic handshake failed”, this is the order I check things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Is the client even attempting QUIC?
&lt;/h3&gt;

&lt;p&gt;If DevTools shows &lt;code&gt;h2&lt;/code&gt;, the browser might not be trying QUIC at all.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was &lt;code&gt;Alt-Svc&lt;/code&gt; present on the first response?&lt;/li&gt;
&lt;li&gt;Is the user on a fresh profile vs a long-lived session?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Did QUIC get attempted but abandoned?
&lt;/h3&gt;

&lt;p&gt;This is where Chrome NetLog is your best weapon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome NetLog&lt;/strong&gt; gives you a JSON file that includes QUIC session events. It’s noisy, but it’s the closest thing to a “flight recorder” you can get without packet capture.&lt;/p&gt;

&lt;p&gt;Workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reproduce the issue in Chrome.&lt;/li&gt;
&lt;li&gt;Export a NetLog.&lt;/li&gt;
&lt;li&gt;Attach it to the incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if you never personally parse it, it gives your networking/SRE folks something concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Check UDP reachability from that network
&lt;/h3&gt;

&lt;p&gt;If UDP is blocked, stop. There’s no magical server config fix.&lt;/p&gt;

&lt;p&gt;Practical actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm the environment: corporate VPN? hotel Wi‑Fi? mobile carrier?&lt;/li&gt;
&lt;li&gt;Try a control test: same URL from another network.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Compare forced H3 vs forced H2 using curl
&lt;/h3&gt;

&lt;p&gt;This is the fastest way to separate “QUIC is broken” from “origin is slow.”&lt;/p&gt;

&lt;p&gt;Capture both transcripts. Post them in the ticket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Pull edge logs for protocol and handshake outcome
&lt;/h3&gt;

&lt;p&gt;If you’re on a CDN, you often can get log fields like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;negotiated protocol (&lt;code&gt;h3&lt;/code&gt; vs &lt;code&gt;h2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;QUIC handshake status&lt;/li&gt;
&lt;li&gt;edge colo&lt;/li&gt;
&lt;li&gt;client IP / ASN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if the exact fields vary by vendor, the goal is consistent: turn “it fell back” into “it fell back for this cohort, on these networks.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Enable origin QUIC logs / qlog (if applicable)
&lt;/h3&gt;

&lt;p&gt;If you control the origin (or you’re using something like nginx QUIC / Envoy QUIC / a QUIC-enabled load balancer), you want structured logs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;qlog&lt;/code&gt; is a common format used by QUIC implementations to record events like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handshake phases&lt;/li&gt;
&lt;li&gt;packet loss&lt;/li&gt;
&lt;li&gt;transport errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enable it, do it intentionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sample it (1% or only for a specific path).&lt;/li&gt;
&lt;li&gt;Time-box it (15–60 minutes).&lt;/li&gt;
&lt;li&gt;Treat it like sensitive telemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure HTTP/3 performance impact (TTFB, handshake time, reuse)
&lt;/h2&gt;

&lt;p&gt;Enabling HTTP/3 can make things faster, but it’s not automatic. If your latency is dominated by origin compute, QUIC won’t save you.&lt;/p&gt;

&lt;p&gt;Here’s how I measure without lying to myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to measure
&lt;/h3&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS&lt;/strong&gt; (did we change resolution behavior?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect / handshake time&lt;/strong&gt; (this is where QUIC can shine)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTFB&lt;/strong&gt; (where origin and caching show up)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content Download&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete numbers to anchor your analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QUIC runs over &lt;strong&gt;UDP&lt;/strong&gt;, typically port &lt;strong&gt;443&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Alt-Svc&lt;/code&gt; caching is in &lt;strong&gt;seconds&lt;/strong&gt; (&lt;code&gt;ma=86400&lt;/code&gt; is one day).&lt;/li&gt;
&lt;li&gt;A “good” difference is not 5 ms on localhost. It’s a consistent delta across percentiles on real networks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to compare H3 vs H2 fairly
&lt;/h3&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compare from the same client machine and network.&lt;/li&gt;
&lt;li&gt;Force protocols with curl, or isolate sessions with incognito.&lt;/li&gt;
&lt;li&gt;Control caching. You want to test both cold cache and warm cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pitfalls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection coalescing&lt;/strong&gt; can mask differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN caching&lt;/strong&gt; can dominate TTFB, making protocol differences vanish.&lt;/li&gt;
&lt;li&gt;Some measurements include service worker behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Browser tooling that actually helps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools Network timings&lt;/strong&gt;: good for quick “is connect time different?” checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance panel&lt;/strong&gt;: useful for separating network from main-thread work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re already doing structured performance work, you might appreciate the mindset in my post on &lt;a href="https://dev.to/blog/llm-latency-benchmark-methodology"&gt;LLM latency&lt;/a&gt; where I argue for decomposing latency into components you can actually act on. Same principle here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log on your CDN and origin to diagnose QUIC
&lt;/h2&gt;

&lt;p&gt;This is the part most teams skip, because it’s “ops work.” Then they act surprised when debugging is impossible.&lt;/p&gt;

&lt;h3&gt;
  
  
  CDN/edge logs: the minimum fields I care about
&lt;/h3&gt;

&lt;p&gt;Even if vendor names differ, I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Negotiated protocol (&lt;code&gt;h3&lt;/code&gt;/&lt;code&gt;h2&lt;/code&gt;/&lt;code&gt;http/1.1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Edge colo / region&lt;/li&gt;
&lt;li&gt;Client ASN (or at least country)&lt;/li&gt;
&lt;li&gt;Upstream connect time and response time&lt;/li&gt;
&lt;li&gt;Error reason when QUIC fails (handshake failure, version mismatch, timeout)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can’t get the QUIC failure reason, at least log the negotiated protocol. Otherwise your incident ends in “seems fine on my machine.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Origin logs: prove what reached you
&lt;/h3&gt;

&lt;p&gt;If you terminate TLS/QUIC at the origin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log whether the request arrived over QUIC or TCP.&lt;/li&gt;
&lt;li&gt;Log connection IDs if your stack exposes them.&lt;/li&gt;
&lt;li&gt;Log handshake failures separately from request failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you terminate at the CDN and talk H2 to origin, then origin won’t see QUIC at all. Don’t waste time looking for it in origin logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  My bias: structured logs over ad-hoc grep
&lt;/h3&gt;

&lt;p&gt;Running this blog’s multi-agent publishing pipeline taught me a painful but useful rule: deterministic gates and structured outputs catch issues earlier than “smart” review ever will. Apply that here. If your QUIC debugging depends on one person remembering a magic grep pattern, you don’t have a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  QUIC retries and 0-RTT: what breaks and how to notice
&lt;/h2&gt;

&lt;p&gt;QUIC has two concepts that matter in production:&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry
&lt;/h3&gt;

&lt;p&gt;A server can force a client to prove address ownership before committing resources. This can add an extra round trip.&lt;/p&gt;

&lt;p&gt;If you suddenly see worse connect times on H3, retries might be happening.&lt;/p&gt;

&lt;p&gt;How to detect without packet capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In NetLog/qlog, look for retry-related events.&lt;/li&gt;
&lt;li&gt;Look for increased “connect” or “handshake” time in DevTools, but stable TTFB once connected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  0-RTT
&lt;/h3&gt;

&lt;p&gt;0-RTT lets a client send application data immediately on a resumed connection. It’s not free.&lt;/p&gt;

&lt;p&gt;What breaks 0-RTT in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS session tickets not being reused (rotation too aggressive, wrong cache scope).&lt;/li&gt;
&lt;li&gt;Load balancers that don’t preserve session resumption state.&lt;/li&gt;
&lt;li&gt;Configuration changes that invalidate tickets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security note: 0-RTT data can be replayed. Don’t allow it for non-idempotent requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleboxes, firewalls, and MTU: network causes you can test
&lt;/h2&gt;

&lt;p&gt;QUIC’s biggest enemy is not your app. It’s the network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common culprits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corporate firewalls&lt;/strong&gt; that block UDP or rate-limit it aggressively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPNs&lt;/strong&gt; that tunnel TCP well but treat UDP as optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Captive portals&lt;/strong&gt; that intercept traffic before the session is established.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path MTU issues&lt;/strong&gt; that blackhole larger QUIC packets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A practical test matrix
&lt;/h3&gt;

&lt;p&gt;When you’re trying to reproduce “HTTP/3 fallback to HTTP/2 troubleshooting” issues, don’t test on one network.&lt;/p&gt;

&lt;p&gt;Test across at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Home Wi‑Fi&lt;/li&gt;
&lt;li&gt;Phone hotspot&lt;/li&gt;
&lt;li&gt;Corporate VPN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s 3 environments. If it fails in only 1 of 3, you’ve learned something actionable.&lt;/p&gt;

&lt;p&gt;Concrete numbers again, because they matter: QUIC is typically UDP &lt;strong&gt;443&lt;/strong&gt;. If your firewall rules allow TCP 443 but block UDP 443, you will get fallback. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A production-first workflow: ship artifacts, not opinions
&lt;/h2&gt;

&lt;p&gt;Here’s the workflow I recommend teams standardize. Not as a tribal “do this sometimes.” As a runbook.&lt;/p&gt;

&lt;h3&gt;
  
  
  The “attachable artifact” bundle
&lt;/h3&gt;

&lt;p&gt;For every HTTP/3 incident, attach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A screenshot (or HAR) showing DevTools protocol negotiation for the failing request.&lt;/li&gt;
&lt;li&gt;A Chrome NetLog JSON captured during repro.&lt;/li&gt;
&lt;li&gt;Two curl transcripts from the same machine/network: forced H3 and forced H2.&lt;/li&gt;
&lt;li&gt;Edge log query results showing protocol distribution and failures for the affected window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That bundle is enough for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SREs to reason about network vs app&lt;/li&gt;
&lt;li&gt;CDN support to escalate&lt;/li&gt;
&lt;li&gt;Developers to reproduce and verify a fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly how I think about &lt;a href="https://dev.to/pillars/ai-engineering-production"&gt;AI in production&lt;/a&gt; too. Debugging is about turning “I think” into “I can show.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: HTTP/3 is easy to enable and hard to &lt;em&gt;prove&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;HTTP/3 is now “on by default” in a lot of stacks, which is precisely why production debugging is painful. When it fails, it fails quietly, falls back, and your dashboards keep looking mostly green.&lt;/p&gt;

&lt;p&gt;My prediction: within 12–18 months, teams will treat “protocol negotiation observability” as a first-class requirement the same way they treat TLS cipher visibility today. If you’re building a performance-sensitive frontend, you should get ahead of that.&lt;/p&gt;

&lt;p&gt;The challenge is simple: next time you claim “we’re on HTTP/3,” attach one artifact that proves it. If you can’t, your system isn’t ready for the next incident.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/debug-http3-quic-production?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=debug-http3-quic-production" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>http3</category>
      <category>quic</category>
      <category>webperf</category>
      <category>networking</category>
    </item>
    <item>
      <title>Transparent Huge Pages + Postgres: Stop P99 Latency Cliffs [2026]</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:44:28 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/transparent-huge-pages-postgres-stop-p99-latency-cliffs-2026-2ll5</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/transparent-huge-pages-postgres-stop-p99-latency-cliffs-2026-2ll5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/transparent-huge-pages-postgres-performance" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Transparent Huge Pages (THP) Postgres performance problems almost never show up as “the database is slow.” They show up as a system that looks fine on averages and then occasionally falls off a cliff at p99. Suddenly you’ve got “random” timeouts, queue backups, and a pager that hates you.&lt;/p&gt;

&lt;p&gt;I’m going to be blunt. If you run self-managed Postgres on Linux and you haven’t made an explicit decision about THP, you’re accepting tail latency risk for no good reason. The kernel is doing work you didn’t ask for, at the exact moment you can least afford it.&lt;/p&gt;

&lt;p&gt;This post is the runbook I wish more teams had: detect → measure → change → validate → enforce. Especially if you’re running Postgres on Kubernetes nodes where config drift is basically a law of nature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;THP is not the same thing as explicit Huge Pages (&lt;code&gt;hugetlbfs&lt;/code&gt;). Confusing them is how teams “tune huge pages” and still keep the latency cliff.&lt;/li&gt;
&lt;li&gt;For OLTP Postgres, the safest default is &lt;code&gt;THP=never&lt;/code&gt; plus &lt;code&gt;defrag=never&lt;/code&gt;. That combo stops background compaction from stalling your backends.&lt;/li&gt;
&lt;li&gt;If you choose &lt;code&gt;madvise&lt;/code&gt;, you still need to control &lt;code&gt;defrag&lt;/code&gt; and verify whether Postgres is actually using THP. Otherwise you’ve just moved the cliff somewhere else.&lt;/li&gt;
&lt;li&gt;Validate with p99/p99.9 latency and &lt;code&gt;pg_stat_statements&lt;/code&gt;, not averages. Averages will lie to you.&lt;/li&gt;
&lt;li&gt;In Kubernetes, this is a &lt;strong&gt;node guardrail&lt;/strong&gt;. If you don’t enforce it, it will drift. And then you’ll “mysteriously” rediscover this post during an incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If your Postgres p99 is spiking “randomly,” assume the kernel is doing work you didn’t ask for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Transparent Huge Pages (THP) is, and why Postgres teams keep getting burned
&lt;/h2&gt;

&lt;p&gt;Transparent Huge Pages (THP) is a Linux kernel feature that automatically backs virtual memory with huge pages (commonly 2 MB) instead of base pages (commonly 4 KB). It can also promote and demote page sizes over time.&lt;/p&gt;

&lt;p&gt;The kernel docs are pretty explicit about scope: THP mainly applies to &lt;strong&gt;anonymous memory mappings&lt;/strong&gt; and &lt;strong&gt;tmpfs/shmem&lt;/strong&gt;, and the intent is “performance without app changes” by handling page promotion/demotion automatically. (&lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html" rel="noopener noreferrer"&gt;Linux kernel docs&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;On paper, that’s a nice deal. Fewer TLB misses. Fewer page faults. Free speed.&lt;/p&gt;

&lt;p&gt;In production with Postgres, it’s usually not a nice deal.&lt;/p&gt;

&lt;p&gt;Postgres is latency-sensitive and memory-active under concurrent load. The thing that bites you isn’t the &lt;em&gt;existence&lt;/em&gt; of huge pages. It’s the kernel doing &lt;strong&gt;automatic promotion plus defrag/compaction work&lt;/strong&gt; at runtime, on your schedule, not its own.&lt;/p&gt;

&lt;p&gt;The kernel docs call out the tradeoff clearly: huge pages reduce TLB misses and reduce page-fault frequency. One page fault per 2 MB region is &lt;strong&gt;512x fewer&lt;/strong&gt; faults than 4 KB pages. But page faults can become more expensive, and the kernel may need background work to create and maintain huge pages. (&lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html" rel="noopener noreferrer"&gt;Linux kernel docs&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;When that background work lands at the wrong moment, you get the classic “latency cliff.” Your median stays boring. Your p99 gets obliterated.&lt;/p&gt;

&lt;h3&gt;
  
  
  THP vs explicit Huge Pages (hugetlbfs) in Postgres
&lt;/h3&gt;

&lt;p&gt;This is where a lot of “performance tuning” content goes off the rails.&lt;/p&gt;

&lt;p&gt;Postgres docs talk about huge pages in the context of kernel resources. In the PostgreSQL 18 docs, &lt;strong&gt;18.4. Managing Kernel Resources&lt;/strong&gt; is the umbrella section, and &lt;strong&gt;18.4.5. Linux Huge Pages&lt;/strong&gt; is the part that’s actually about &lt;em&gt;explicit&lt;/em&gt; huge pages, typically pre-allocated and managed intentionally. (&lt;a href="https://www.postgresql.org/docs/current/kernel-resources.html" rel="noopener noreferrer"&gt;PostgreSQL docs: 18.4 Managing Kernel Resources&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Explicit huge pages (&lt;code&gt;hugetlbfs&lt;/code&gt;) are predictable. You provision them. You monitor them. You know what you’re getting.&lt;/p&gt;

&lt;p&gt;Transparent huge pages are the opposite. They’re opportunistic and adaptive. That’s fine for some workloads. For OLTP Postgres, it’s where tail latency goes to die.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why THP causes Postgres p99 spikes even when the average looks fine
&lt;/h2&gt;

&lt;p&gt;Here’s the failure mode that keeps repeating in real systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Everything looks stable. Memory pressure is moderate but not catastrophic.&lt;/li&gt;
&lt;li&gt;THP is enabled in &lt;code&gt;always&lt;/code&gt;, so the kernel tries to allocate and maintain huge pages.&lt;/li&gt;
&lt;li&gt;The kernel background thread (&lt;code&gt;khugepaged&lt;/code&gt;) scans memory and tries to collapse 4 KB pages into 2 MB huge pages.&lt;/li&gt;
&lt;li&gt;When it can’t find a contiguous 2 MB region easily, it triggers compaction/defrag work.&lt;/li&gt;
&lt;li&gt;Compaction can stall processes. Those stalls show up as &lt;strong&gt;tail latency spikes&lt;/strong&gt; in Postgres queries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not some spooky “kernel magic.” It’s exactly what the THP design implies: promotion/demotion costs real CPU and can involve memory compaction. The sysfs knobs under &lt;code&gt;/sys/kernel/mm/transparent_hugepage/&lt;/code&gt; exist because the kernel authors know these tradeoffs matter. (&lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html#admin-guide-transhuge" rel="noopener noreferrer"&gt;Linux kernel docs: THP sysfs knobs and modes&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So why do averages look fine?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most queries never hit the stall.&lt;/li&gt;
&lt;li&gt;When it happens, it’s intermittent.&lt;/li&gt;
&lt;li&gt;Your dashboards default to rolled-up means because observability vendors love pretty lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And why does p99 explode?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A few unlucky backends get paused during compaction.&lt;/li&gt;
&lt;li&gt;Under load, one stalled backend turns into queues, lock waits, and cascading slowdowns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tail latency is where “the DB is fine” turns into a user-facing outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exact THP modes and the recommended settings for Postgres OLTP
&lt;/h2&gt;

&lt;p&gt;Linux exposes two knobs you actually care about for this topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/sys/kernel/mm/transparent_hugepage/enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/sys/kernel/mm/transparent_hugepage/defrag&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The modes you’ll see are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;always&lt;/code&gt;: the kernel aggressively uses THP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;madvise&lt;/code&gt;: the kernel uses THP only when applications request it via &lt;code&gt;madvise()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;never&lt;/code&gt;: the kernel doesn’t use THP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those names aren’t blog-invented. They’re straight from the kernel docs. (&lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html#admin-guide-transhuge" rel="noopener noreferrer"&gt;Linux kernel docs&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  My opinionated defaults (the ones that stop latency cliffs)
&lt;/h3&gt;

&lt;p&gt;If you’re running OLTP Postgres (user-facing requests, spiky concurrency, real SLOs), the default that prevents most THP-induced cliffs is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enabled = never&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defrag = never&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a specific reason to keep THP around (some analytics-heavy mixed workloads, some VM setups), the least dangerous option is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enabled = madvise&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defrag = never&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re currently on &lt;code&gt;always&lt;/code&gt;, you’re basically telling the kernel: “Feel free to do background memory work whenever you want.” That’s not a production posture. That’s a dare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe defaults matrix (what you can actually change)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;What you control&lt;/th&gt;
&lt;th&gt;Recommended THP setting for Postgres OLTP&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bare metal, self-managed Postgres&lt;/td&gt;
&lt;td&gt;Full kernel params&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;never&lt;/code&gt; + &lt;code&gt;defrag=never&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Most deterministic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM (cloud or on-prem)&lt;/td&gt;
&lt;td&gt;Usually full kernel params&lt;/td&gt;
&lt;td&gt;Start with &lt;code&gt;never&lt;/code&gt; + &lt;code&gt;defrag=never&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Huge pages can help some virtualization overhead, but OLTP tail latency matters more.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes (self-managed nodes)&lt;/td&gt;
&lt;td&gt;Node kernel params&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;never&lt;/code&gt; + &lt;code&gt;defrag=never&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Enforce at node boot. Don’t try to “fix” inside the container.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Postgres (RDS/Cloud SQL/Aurora/etc.)&lt;/td&gt;
&lt;td&gt;Almost nothing&lt;/td&gt;
&lt;td&gt;You can’t change THP&lt;/td&gt;
&lt;td&gt;Don’t waste time hunting sysfs knobs you don’t own. Focus on query and schema tuning.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: Check system-wide THP usage (and don’t guess)
&lt;/h2&gt;

&lt;p&gt;You want two facts, not vibes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Current mode (&lt;code&gt;enabled&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Current defrag behavior (&lt;code&gt;defrag&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On most distros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cat /sys/kernel/mm/transparent_hugepage/enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat /sys/kernel/mm/transparent_hugepage/defrag&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll see something like &lt;code&gt;always madvise [never]&lt;/code&gt; where the brackets mark the active mode.&lt;/p&gt;

&lt;p&gt;That’s the kernel’s sysfs interface doing exactly what it says on the tin. (&lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html#admin-guide-transhuge" rel="noopener noreferrer"&gt;Linux kernel docs: THP sysfs knobs and modes&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Also check whether &lt;code&gt;khugepaged&lt;/code&gt; is active and burning CPU during incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ps -eo pid,comm,pcpu,pmem,args | grep khugepaged&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;top&lt;/code&gt; / &lt;code&gt;htop&lt;/code&gt; during a spike and look for &lt;code&gt;khugepaged&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;khugepaged&lt;/code&gt; CPU jumps at the same time your p99 jumps, that’s not “proof,” but it’s a pretty loud hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Check THP usage per process (is Postgres actually using it?)
&lt;/h2&gt;

&lt;p&gt;System-wide mode is necessary, but it’s not sufficient. You also want to know what’s happening inside the Postgres process.&lt;/p&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify the Postgres PID (or the &lt;code&gt;postmaster&lt;/code&gt; PID)&lt;/li&gt;
&lt;li&gt;Inspect &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps&lt;/code&gt; or &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps_rollup&lt;/code&gt; and look for huge page indicators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On RHEL-family systems, Red Hat’s runbook includes practical steps for checking system-wide and per-process THP usage. This is one of those rare cases where the boring enterprise doc is the right reference because it’s written for operators, not for Twitter. (&lt;a href="https://access.redhat.com/solutions/46111" rel="noopener noreferrer"&gt;Red Hat KB 46111&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Why you care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you set &lt;code&gt;madvise&lt;/code&gt; but nothing in your stack requests THP, you might be chasing a ghost.&lt;/li&gt;
&lt;li&gt;If you set &lt;code&gt;never&lt;/code&gt; but a tuned profile flips it back on, you’ll want receipts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Benchmark before you change anything (pgbench + p99)
&lt;/h2&gt;

&lt;p&gt;If you change kernel memory behavior in prod without a baseline, you’re doing performance cosplay.&lt;/p&gt;

&lt;p&gt;Here’s the loop I like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capture p50/p95/p99/p99.9 latencies from your app or from &lt;code&gt;pgbench&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Capture a 5–15 minute window of &lt;code&gt;pg_stat_statements&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make the THP change.&lt;/li&gt;
&lt;li&gt;Repeat the same measurement window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You’re looking for a very specific shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Median might not move much.&lt;/li&gt;
&lt;li&gt;p99/p99.9 should smooth out.&lt;/li&gt;
&lt;li&gt;Query max times in &lt;code&gt;pg_stat_statements&lt;/code&gt; should drop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use an actual number for the run. For example: run &lt;code&gt;pgbench&lt;/code&gt; for &lt;strong&gt;10 minutes&lt;/strong&gt; at a concurrency of &lt;strong&gt;64&lt;/strong&gt; and capture latency percentiles.&lt;/p&gt;

&lt;p&gt;If your app has a known “spiky hour” (batch jobs, traffic peak, cron chaos), measure during that. Don’t benchmark at 2 p.m. and declare victory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Apply the fix — disable THP at run time (immediate mitigation)
&lt;/h2&gt;

&lt;p&gt;When you’re in an incident, you want the change &lt;strong&gt;now&lt;/strong&gt;, even if it won’t persist.&lt;/p&gt;

&lt;p&gt;Runtime mitigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write &lt;code&gt;never&lt;/code&gt; to &lt;code&gt;/sys/kernel/mm/transparent_hugepage/enabled&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Write &lt;code&gt;never&lt;/code&gt; to &lt;code&gt;/sys/kernel/mm/transparent_hugepage/defrag&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the operational equivalent of pulling the emergency brake. It won’t survive a reboot. That’s fine. The goal is to stop the cliff and stabilize.&lt;/p&gt;

&lt;p&gt;Red Hat’s procedure covers the runtime disable path and it’s worth following if you’re on RHEL. (&lt;a href="https://access.redhat.com/solutions/46111" rel="noopener noreferrer"&gt;Red Hat KB 46111&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Make it persistent — disable THP at boot time (the real fix)
&lt;/h2&gt;

&lt;p&gt;If you don’t persist it, it will come back. And yes, it will come back at 3 a.m.&lt;/p&gt;

&lt;p&gt;You’ve got a few durable options. Which one you use depends on what your fleet standards look like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: GRUB kernel parameters (works broadly)
&lt;/h3&gt;

&lt;p&gt;Set the kernel command line so THP is disabled at boot.&lt;/p&gt;

&lt;p&gt;Common pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;transparent_hugepage=never&lt;/code&gt; to the kernel cmdline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then regenerate GRUB config and reboot.&lt;/p&gt;

&lt;p&gt;This is the most reliable “set it and forget it” method across distros.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: systemd-tmpfiles (cleaner than rc.local)
&lt;/h3&gt;

&lt;p&gt;On systemd hosts, you can write the sysfs knobs at boot via a tmpfiles rule.&lt;/p&gt;

&lt;p&gt;This avoids resurrecting &lt;code&gt;rc.local&lt;/code&gt; hacks and keeps the change declarative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: tuned profiles (RHEL ecosystems)
&lt;/h3&gt;

&lt;p&gt;If you use &lt;code&gt;tuned&lt;/code&gt;, be careful. Some profiles override THP behavior. The worst case is thinking you disabled THP and then a tuned profile quietly flips it back.&lt;/p&gt;

&lt;p&gt;Again, Red Hat is explicit about monitoring and disabling THP in an enterprise environment. Follow it if that’s your world. (&lt;a href="https://access.redhat.com/solutions/46111" rel="noopener noreferrer"&gt;Red Hat KB 46111&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Validate with pg_stat_statements (Postgres 18/19-era workflow)
&lt;/h2&gt;

&lt;p&gt;If you’re not using &lt;code&gt;pg_stat_statements&lt;/code&gt;, you’re debugging Postgres blind.&lt;/p&gt;

&lt;p&gt;The official docs are clear: &lt;code&gt;pg_stat_statements&lt;/code&gt; must be loaded via &lt;code&gt;shared_preload_libraries&lt;/code&gt; (so yes, you need a restart). It tracks planning and execution stats, and it relies on query identifier calculation, which is enabled when &lt;code&gt;compute_query_id&lt;/code&gt; is &lt;code&gt;auto&lt;/code&gt; or &lt;code&gt;on&lt;/code&gt;. (&lt;a href="https://www.postgresql.org/docs/current/pgstatstatements.html" rel="noopener noreferrer"&gt;PostgreSQL docs: pg_stat_statements&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That’s the 2026 angle that actually matters here: newer Postgres makes statement-level instrumentation less annoying, so you have fewer excuses.&lt;/p&gt;

&lt;h3&gt;
  
  
  F.32. pg_stat_statements — track statistics of SQL planning and execution
&lt;/h3&gt;

&lt;p&gt;That’s literally the section title in the docs, and it’s exactly what you want. You’re not chasing a slightly faster mean. You’re trying to stop unpredictable stalls.&lt;/p&gt;

&lt;h3&gt;
  
  
  F.32.1. The pg_stat_statements View
&lt;/h3&gt;

&lt;p&gt;The view has the columns you need to validate tail improvements: min/mean/max times, total time, and call counts. Rows are keyed by &lt;code&gt;(dbid, userid, queryid, toplevel)&lt;/code&gt;. (&lt;a href="https://www.postgresql.org/docs/current/pgstatstatements.html" rel="noopener noreferrer"&gt;PostgreSQL docs: pg_stat_statements&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;What I pull in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top queries by total execution time (to see if anything regressed)&lt;/li&gt;
&lt;li&gt;Top queries by max execution time (to catch tail cliffs)&lt;/li&gt;
&lt;li&gt;A before/after comparison window around the change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also: reset stats when you do controlled experiments. Otherwise you’re mixing pre-change and post-change behavior and pretending it’s science.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Validate with p99/p99.9 latency, not just statement stats
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; is necessary, but it’s not the same thing as end-to-end latency.&lt;/p&gt;

&lt;p&gt;What I like to validate after the change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App-level request p99 and p99.9 over at least &lt;strong&gt;24 hours&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;DB pool wait time (if you have it)&lt;/li&gt;
&lt;li&gt;Postgres wait events distribution (newer Postgres versions make this more useful)&lt;/li&gt;
&lt;li&gt;OS signals: &lt;code&gt;khugepaged&lt;/code&gt; CPU drops, fewer compaction stalls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If p99 got better but throughput dropped, you may have traded one bottleneck for another. That’s not the common outcome with THP changes, but don’t assume you’re immune.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Kubernetes guardrails — enforce THP settings on nodes and prevent drift
&lt;/h2&gt;

&lt;p&gt;In Kubernetes, the most common failure mode is trying to “disable THP inside the container.” That’s not how kernels work.&lt;/p&gt;

&lt;p&gt;THP is a node-level setting. Treat it like &lt;code&gt;vm.swappiness&lt;/code&gt; or overcommit policy. It lives with the node.&lt;/p&gt;

&lt;p&gt;Two patterns that actually work:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Node bootstrap / machine config (preferred)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If you run OpenShift, use a MachineConfig to set kernel args.&lt;/li&gt;
&lt;li&gt;If you run Cluster API or managed node groups with custom AMIs, bake the kernel args into the image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the highest-integrity approach. Nodes come up correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Privileged DaemonSet (enforcement + audit)
&lt;/h3&gt;

&lt;p&gt;If you can’t bake images quickly, use a privileged DaemonSet that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks &lt;code&gt;/sys/kernel/mm/transparent_hugepage/enabled&lt;/code&gt; and &lt;code&gt;/defrag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Writes the desired value if it’s wrong&lt;/li&gt;
&lt;li&gt;Exposes a node condition or metric so you can alert on drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be realistic: it’s still a band-aid compared to doing it at boot. But it’s miles better than hope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift prevention checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Alert if any node reports &lt;code&gt;always&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Alert if &lt;code&gt;defrag&lt;/code&gt; is not &lt;code&gt;never&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Gate node pools. Don’t schedule Postgres pods on nodes that fail the check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re doing this for a mission-critical database, treat it like a compliance control, not a one-time tweak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra guardrails: kernel knobs that correlate with THP-related stalls
&lt;/h2&gt;

&lt;p&gt;THP issues usually show up next to a couple other kernel behaviors. You don’t need to “tune the whole OS,” but you should know what tends to correlate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The defrag knob is the big one
&lt;/h3&gt;

&lt;p&gt;If I had to pick a single “stop the cliff” setting besides disabling THP entirely, it’s &lt;code&gt;defrag=never&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That tells the kernel: don’t do expensive compaction work in the background just to satisfy huge page allocations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch for memory pressure and compaction signals
&lt;/h3&gt;

&lt;p&gt;Correlate Postgres p99 spikes with OS-level signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;khugepaged&lt;/code&gt; CPU usage&lt;/li&gt;
&lt;li&gt;Memory compaction activity&lt;/li&gt;
&lt;li&gt;Swap activity (even small amounts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see compaction and swap during p99 spikes, you don’t have a Postgres problem. You have a node problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together: the copy/paste runbook
&lt;/h2&gt;

&lt;p&gt;Here’s the exact loop I’d run, in order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Record baseline&lt;/strong&gt;: p99/p99.9 latency for &lt;strong&gt;30 minutes&lt;/strong&gt; during representative load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check THP system-wide&lt;/strong&gt;: read &lt;code&gt;/sys/kernel/mm/transparent_hugepage/enabled&lt;/code&gt; and &lt;code&gt;/defrag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check per-process&lt;/strong&gt;: confirm whether the Postgres PID is using THP via &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps(_rollup)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable measurement&lt;/strong&gt;: ensure &lt;code&gt;pg_stat_statements&lt;/code&gt; is on (restart required). (&lt;a href="https://www.postgresql.org/docs/current/pgstatstatements.html" rel="noopener noreferrer"&gt;PostgreSQL docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mitigate&lt;/strong&gt;: set &lt;code&gt;enabled=never&lt;/code&gt; and &lt;code&gt;defrag=never&lt;/code&gt; at runtime to stop an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist&lt;/strong&gt;: set boot-time config via GRUB or a node image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt;: compare before/after p99 and &lt;code&gt;pg_stat_statements&lt;/code&gt; max times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce&lt;/strong&gt; (Kubernetes): machine config or privileged DaemonSet plus alerts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: make THP a deliberate decision, or the kernel will decide for you
&lt;/h2&gt;

&lt;p&gt;The industry has a weird habit of treating kernel settings like folklore. THP isn’t folklore. It has documented modes. It has documented knobs. And for OLTP Postgres, the default Linux behavior is often the wrong one.&lt;/p&gt;

&lt;p&gt;So don’t argue about THP in theory. Run the loop. Measure p99. Change one knob. Measure again.&lt;/p&gt;

&lt;p&gt;My bet for the next year: as more teams run Postgres on multi-tenant Kubernetes node pools, THP misconfiguration becomes the new “noisy neighbor” incident class. The teams that win won’t be the ones with the fanciest database. They’ll be the ones who turned kernel behavior into an enforced guardrail.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/transparent-huge-pages-postgres-performance?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=transparent-huge-pages-postgres-performance" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>linux</category>
      <category>performancetuning</category>
      <category>clouddevops</category>
    </item>
    <item>
      <title>Data Privacy in RAG Redaction and Retention [2026 Playbook]</title>
      <dc:creator>Kunal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:44:18 +0000</pubDate>
      <link>https://dev.to/kunal_d6a8fea2309e1571ee7/data-privacy-in-rag-redaction-and-retention-2026-playbook-72l</link>
      <guid>https://dev.to/kunal_d6a8fea2309e1571ee7/data-privacy-in-rag-redaction-and-retention-2026-playbook-72l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://www.kunalganglani.com/blog/data-privacy-rag-redaction-retention" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt; — read it there for inline code, hero image, and live links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Data privacy in rag redaction and retention is not a “security checklist” problem. It’s an architecture problem.&lt;/p&gt;

&lt;p&gt;Ship Retrieval-Augmented Generation (RAG) into anything regulated and you find out fast: your privacy posture is defined by a dozen tiny boundaries. Ingestion. Chunking. Embeddings. Retrieval filters. Prompt construction. Tool calls. Model outputs.&lt;/p&gt;

&lt;p&gt;And then the graveyard where everything goes to die.&lt;/p&gt;

&lt;p&gt;Logs, traces, analytics, replay systems. The stuff teams keep “just for a week” until it quietly becomes 180 days.&lt;/p&gt;

&lt;p&gt;Most teams get one boundary right (usually “don’t train on my data”) and then leak sensitive data everywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A RAG system has at least &lt;strong&gt;8 privacy leak points&lt;/strong&gt;. If you only redact at ingestion, you will still leak via prompts, logs, and traces.&lt;/li&gt;
&lt;li&gt;Redact &lt;strong&gt;PII/PHI/PCI&lt;/strong&gt; and &lt;strong&gt;secrets&lt;/strong&gt; differently. Secrets require &lt;em&gt;prevention&lt;/em&gt; and &lt;em&gt;blocking&lt;/em&gt;, not just masking.&lt;/li&gt;
&lt;li&gt;Use a retention matrix. Default to &lt;strong&gt;7–30 days&lt;/strong&gt; for operational traces, &lt;strong&gt;0–7 days&lt;/strong&gt; for raw prompts, and &lt;strong&gt;90–365 days&lt;/strong&gt; for compliance audit events that contain &lt;em&gt;no raw text&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Treat embeddings as sensitive artifacts. They’re not “random vectors.” Retention and deletion must be designed, not wished into existence.&lt;/li&gt;
&lt;li&gt;The best audit trail is structured, hashed, and policy-aware. It should prove what happened without storing what you wish never happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you can’t delete it, don’t collect it. And if you need it for debugging, collect a safer version.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What is RAG and where privacy leaks happen
&lt;/h2&gt;

&lt;p&gt;Retrieval-Augmented Generation (RAG) is the pattern where a model answers a user’s question using external knowledge retrieved at request time. Instead of fine-tuning a model on your private docs, you fetch relevant chunks from a search index or vector database and inject them into the prompt.&lt;/p&gt;

&lt;p&gt;That’s why RAG is so attractive in enterprise and regulated domains. It’s also why privacy failures get… sneaky.&lt;/p&gt;

&lt;p&gt;In a typical production pipeline, sensitive data can leak at &lt;strong&gt;eight&lt;/strong&gt; places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source documents&lt;/strong&gt; (PDFs, tickets, call transcripts, emails)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunked text&lt;/strong&gt; (the “cleaned” form engineers often forget is still raw text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; (vectors derived from sensitive text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector DB metadata&lt;/strong&gt; (tenant IDs, doc IDs, ACLs, tags)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval results&lt;/strong&gt; (the top-k chunks your system thinks are relevant)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt assembly&lt;/strong&gt; (where you concatenate user query + system prompt + retrieved context)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation output&lt;/strong&gt; (which can repeat or transform sensitive data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; (logs, traces, analytics events, replay systems)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’ve built RAG systems that handle &lt;strong&gt;millions of queries daily&lt;/strong&gt; with &lt;strong&gt;sub-second response times&lt;/strong&gt; for the Walmart conversational commerce chatbot. At that scale, the privacy problem stops being theoretical. One overly-verbose trace attribute doesn’t leak “a little.” It leaks thousands of secrets per hour. Not because anyone is evil. Because volume turns every mistake into an incident.&lt;/p&gt;

&lt;p&gt;The 2026 change is that AI governance is finally becoming operational. Teams are standardizing &lt;strong&gt;policy-as-code&lt;/strong&gt; for prompt/context filtering and adopting &lt;strong&gt;privacy-preserving tracing&lt;/strong&gt; (hashed identifiers, structured audit events) to satisfy audits without hoarding raw prompts.&lt;/p&gt;

&lt;p&gt;Here’s the playbook I wish more teams shipped with.&lt;/p&gt;




&lt;h2&gt;
  
  
  PII and secrets: what to redact (and what not to)
&lt;/h2&gt;

&lt;p&gt;The fastest way to build a broken privacy system is to treat “sensitive data” like it’s one big bucket.&lt;/p&gt;

&lt;p&gt;In RAG, you usually have &lt;strong&gt;four&lt;/strong&gt; categories that behave very differently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PII&lt;/strong&gt; (names, emails, phone numbers, addresses, government IDs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHI&lt;/strong&gt; (health info, diagnoses, prescriptions, patient IDs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PCI&lt;/strong&gt; (card numbers, CVV, bank account identifiers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt; (API keys, OAuth tokens, session cookies, private keys, internal credentials)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus a fifth that matters in enterprise but gets hand-waved way too often:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Proprietary text&lt;/strong&gt; (contracts, incident reports, roadmap docs, source code)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Redact PII/PHI/PCI, but don’t destroy meaning
&lt;/h3&gt;

&lt;p&gt;PII redaction in RAG pipelines is about &lt;strong&gt;reducing risk without turning your index into mush&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you delete every name, address, and number, retrieval quality falls off a cliff. And in production RAG, retrieval quality is the whole game.&lt;/p&gt;

&lt;p&gt;I learned this building the Walmart RAG pipeline. Model choice mattered, but &lt;strong&gt;retrieval quality dominated&lt;/strong&gt; at scale. If your privacy controls crater retrieval, the “fix” the product team reaches for is predictable. “Increase k.” “Log more.” “Add more context.” Congratulations, you just made privacy worse.&lt;/p&gt;

&lt;p&gt;So for PII/PHI/PCI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer &lt;strong&gt;token-preserving masking&lt;/strong&gt; over deletion. Replace names with stable placeholders like &lt;code&gt;PERSON_1&lt;/code&gt;, &lt;code&gt;PERSON_2&lt;/code&gt; &lt;em&gt;within a single document&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Preserve &lt;strong&gt;document structure&lt;/strong&gt; (headings, tables, bullet points). Structure is signal.&lt;/li&gt;
&lt;li&gt;Keep &lt;strong&gt;non-sensitive numeric values&lt;/strong&gt; when they’re essential (prices, dimensions, dates in public policies). Over-redaction creates useless context and forces more retrieval.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Secrets are different: block, rotate, and alert
&lt;/h3&gt;

&lt;p&gt;Secrets detection in LLM systems is not mainly a “masking” problem. It’s an &lt;em&gt;incident prevention&lt;/em&gt; problem.&lt;/p&gt;

&lt;p&gt;Once a credential gets into the pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it can land in logs,&lt;/li&gt;
&lt;li&gt;it can get echoed by the model,&lt;/li&gt;
&lt;li&gt;it can get cached,&lt;/li&gt;
&lt;li&gt;it can end up in an eval dataset,&lt;/li&gt;
&lt;li&gt;it can be pasted into a ticket by someone trying to “help.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect and &lt;strong&gt;hard-block&lt;/strong&gt; known secret formats (API keys, JWTs, bearer tokens, private keys).&lt;/li&gt;
&lt;li&gt;If a secret shows up in user input, respond with a safe error and trigger rotation workflows.&lt;/li&gt;
&lt;li&gt;If a secret shows up in retrieved context (yes, this happens), quarantine the source doc and re-index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical rule: if it looks like a credential, treat it like a production incident. Not like a string to redact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redaction enforcement points: ingestion, retrieval, prompt construction, and logging
&lt;/h2&gt;

&lt;p&gt;Teams always ask: “Should we redact before embedding or at retrieval time, or both?”&lt;/p&gt;

&lt;p&gt;My stance: &lt;strong&gt;both&lt;/strong&gt;. But for different reasons. If you pick one, you’ll end up compensating somewhere else. Usually in logging. And that’s how leaks become permanent.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Ingestion-time redaction (before chunking and embedding)
&lt;/h3&gt;

&lt;p&gt;Ingestion-time redaction is your first shot at reducing blast radius.&lt;/p&gt;

&lt;p&gt;Do it here because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw docs are the richest leak source,&lt;/li&gt;
&lt;li&gt;you can run heavier detectors (DLP, regex + ML, custom rules),&lt;/li&gt;
&lt;li&gt;you can store &lt;em&gt;two&lt;/em&gt; versions: a raw vault copy (restricted) and a redacted RAG copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In healthcare or finance, it’s common to keep raw docs in a separate evidence vault with a different retention policy. Your RAG path should almost never touch raw.&lt;/p&gt;

&lt;p&gt;Ingestion-time output should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redacted chunks for indexing&lt;/li&gt;
&lt;li&gt;a redaction manifest (what was removed, by what rule, at what time)&lt;/li&gt;
&lt;li&gt;provenance metadata (source system, owner, tenant, classification)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Retrieval-time filtering (tenant, ACL, and policy checks)
&lt;/h3&gt;

&lt;p&gt;Retrieval is where multi-tenant isolation either works… or you end up on a call you don’t want.&lt;/p&gt;

&lt;p&gt;Your retriever must enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenant isolation&lt;/strong&gt; (hard partition or mandatory tenant filter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document-level ACLs&lt;/strong&gt; (user can only retrieve what they can access)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy filters&lt;/strong&gt; (e.g., “this user role cannot retrieve PHI”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not rely on the LLM to “refuse” forbidden data. That’s like asking a logging library to enforce RBAC.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Prompt-time redaction (last-mile safety)
&lt;/h3&gt;

&lt;p&gt;Prompt construction is the last place you control the content before it hits a model API.&lt;/p&gt;

&lt;p&gt;This is where you catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;residual PII from ingestion failures,&lt;/li&gt;
&lt;li&gt;secrets via user input or retrieved text,&lt;/li&gt;
&lt;li&gt;prompt injection payloads trying to force exfiltration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re already doing prompt filtering for &lt;a href="https://dev.to/blog/prompt-injection-2026-owasp-llm-vulnerability"&gt;prompt injection&lt;/a&gt;, extend the same policy engine to redact sensitive spans. Same machinery. Different rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Logging-time minimization (where most leaks actually happen)
&lt;/h3&gt;

&lt;p&gt;LLM prompt logging retention policy is where teams self-own.&lt;/p&gt;

&lt;p&gt;It usually starts with a reasonable intention: “we need traces to debug hallucinations.”&lt;/p&gt;

&lt;p&gt;Then you log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the full user query&lt;/li&gt;
&lt;li&gt;the full retrieved chunks&lt;/li&gt;
&lt;li&gt;the full prompt&lt;/li&gt;
&lt;li&gt;the full model output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and you keep it for &lt;strong&gt;180 days&lt;/strong&gt; because “compliance.”&lt;/p&gt;

&lt;p&gt;That’s not compliance. That’s a breach backlog with a nice dashboard.&lt;/p&gt;

&lt;p&gt;The fix is to split observability into two streams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compliance audit events&lt;/strong&gt;: structured, minimal, long retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug traces&lt;/strong&gt;: richer, short retention, access-gated, redacted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote a full schema approach in &lt;a href="https://dev.to/blog/ai-agent-observability-logging-schema"&gt;AI Agent Observability Logging Schema [2026]: OTel + Redaction&lt;/a&gt;. This post is the same idea, applied specifically to RAG.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retention policies for RAG artifacts (documents, embeddings, prompts, traces)
&lt;/h2&gt;

&lt;p&gt;Retention gets political fast. Security wants “keep nothing.” Debugging wants “keep everything.” Legal wants “keep whatever the policy says, and prove you did it.”&lt;/p&gt;

&lt;p&gt;This is one of those things where the boring answer is actually the right one. Build a retention matrix. Implement it with TTLs, deletion jobs, and an audit trail that doesn’t rely on storing raw content forever.&lt;/p&gt;

&lt;p&gt;Here’s a default retention matrix you can start with. Adjust by domain and regulation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Default retention matrix (practical starting point)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Contains raw text?&lt;/th&gt;
&lt;th&gt;Sensitivity risk&lt;/th&gt;
&lt;th&gt;Suggested default retention&lt;/th&gt;
&lt;th&gt;Implementation pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw source docs (vault)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;1–7 years (policy-driven)&lt;/td&gt;
&lt;td&gt;Separate storage + strict access + legal hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redacted docs/chunks (RAG store)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;90–365 days&lt;/td&gt;
&lt;td&gt;Object store TTL + re-ingestion pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;No (but derived)&lt;/td&gt;
&lt;td&gt;Medium–high&lt;/td&gt;
&lt;td&gt;90–365 days&lt;/td&gt;
&lt;td&gt;Vector DB TTL per record + re-embed on refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector metadata (ACL, tenant)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;As long as embedding&lt;/td&gt;
&lt;td&gt;Same lifecycle as embedding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User queries&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;0–30 days&lt;/td&gt;
&lt;td&gt;Store redacted form; hash identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assembled prompts&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;0–7 days&lt;/td&gt;
&lt;td&gt;Prefer not storing; store prompt hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieved context snippets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;0–7 days&lt;/td&gt;
&lt;td&gt;Store doc IDs + chunk hashes, not text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model outputs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;0–30 days&lt;/td&gt;
&lt;td&gt;Store redacted summaries; avoid raw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug traces (full request graph)&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;7–30 days&lt;/td&gt;
&lt;td&gt;Separate sink + access control + sampling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance audit events&lt;/td&gt;
&lt;td&gt;No (structured)&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;td&gt;1–3 years&lt;/td&gt;
&lt;td&gt;Append-only store, tamper-evident&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You’ll notice two things:&lt;/p&gt;

&lt;p&gt;1) The stuff you &lt;em&gt;want&lt;/em&gt; for debugging (prompts, retrieved context) has the shortest retention.&lt;/p&gt;

&lt;p&gt;2) The long-retention trail is mostly &lt;strong&gt;IDs, hashes, and policy decisions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s how you get both: debuggability and defensible privacy. Anything else turns into either blind debugging or an accidental data lake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do embeddings contain PII? How should embeddings be treated?
&lt;/h3&gt;

&lt;p&gt;Embeddings are derived from sensitive text. They can leak information via membership inference or reconstruction attacks under certain conditions. And they absolutely count as “personal data” in many governance programs because they’re linked to identifiers and can be used to profile.&lt;/p&gt;

&lt;p&gt;Practically: treat embeddings as &lt;strong&gt;sensitive artifacts&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put them under retention.&lt;/li&gt;
&lt;li&gt;Put them under deletion.&lt;/li&gt;
&lt;li&gt;Encrypt them.&lt;/li&gt;
&lt;li&gt;Restrict access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your design assumes embeddings are harmless, you will eventually build “vector analytics” that becomes a shadow data warehouse. It happens slowly, then all at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  GDPR/CCPA deletion in a vector database: the part everyone avoids
&lt;/h3&gt;

&lt;p&gt;If you support deletion requests, you need a real answer to: “How do we delete a person’s data from the vector index?”&lt;/p&gt;

&lt;p&gt;There are only a few options that actually work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hard delete by document IDs&lt;/strong&gt;: maintain a mapping from source doc → chunk IDs → embedding IDs. Cleanest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tombstones + async purge&lt;/strong&gt;: mark embeddings deleted, exclude at query time, purge later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-embedding&lt;/strong&gt;: if PII was embedded into shared chunks, you may need to re-chunk and re-embed affected documents.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Operational reality: deletion is a pipeline, not a database call.&lt;/p&gt;

&lt;p&gt;Design your indexing around &lt;strong&gt;stable IDs&lt;/strong&gt; and &lt;strong&gt;lineage&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source document ID&lt;/li&gt;
&lt;li&gt;chunk ID&lt;/li&gt;
&lt;li&gt;embedding ID&lt;/li&gt;
&lt;li&gt;tenant ID&lt;/li&gt;
&lt;li&gt;classification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That lineage is also what makes audit trails possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit trails for RAG: what to log for compliance vs debugging
&lt;/h2&gt;

&lt;p&gt;A good RAG audit trail answers two questions:&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;What happened?&lt;/strong&gt; (who queried, what policies applied, which documents were accessed)&lt;br&gt;
2) &lt;strong&gt;Can you prove it?&lt;/strong&gt; (immutability, integrity, reproducibility)&lt;/p&gt;

&lt;p&gt;A bad audit trail is “we stored every prompt for 2 years.” That’s not an audit trail. That’s a liability with extra steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should an audit trail contain for RAG?
&lt;/h3&gt;

&lt;p&gt;Here’s an audit event schema I’ve seen hold up well. It’s intentionally not raw-text.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;request_id&lt;/code&gt; (correlation ID)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tenant_id&lt;/code&gt; (or hashed tenant)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actor_id&lt;/code&gt; (hashed user ID)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actor_role&lt;/code&gt; (admin, support, end-user)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client_app&lt;/code&gt; (web, mobile, internal tool)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;query_class&lt;/code&gt; (support, product Q&amp;amp;A, medical, financial)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retrieval_policy_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redaction_policy_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model_provider&lt;/code&gt; + &lt;code&gt;model_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prompt_template_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retrieved_doc_ids&lt;/code&gt; (list)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retrieved_chunk_ids&lt;/code&gt; (list)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retrieved_chunk_hashes&lt;/code&gt; (optional)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decision_flags&lt;/code&gt; (blocked_secret, blocked_phi, allowlist_hit, denylist_hit)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output_classification&lt;/code&gt; (safe, contains_pii_suspected, blocked)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latency_ms&lt;/code&gt; (end-to-end)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what’s missing: the user’s full query and the raw retrieved text.&lt;/p&gt;

&lt;p&gt;If you need query-level detail for abuse investigations, store a &lt;strong&gt;redacted query&lt;/strong&gt; or a &lt;strong&gt;hash&lt;/strong&gt; plus a short-lived encrypted debug packet in a separate system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate compliance logs from debug traces
&lt;/h3&gt;

&lt;p&gt;This separation is not optional in regulated environments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compliance logs are append-only, long retention, minimal data.&lt;/li&gt;
&lt;li&gt;Debug traces are short retention, sampled, access-gated, and aggressively redacted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t split them, your compliance system becomes your highest-risk data store.&lt;/p&gt;

&lt;p&gt;This is also where modern tracing helps. When you instrument your RAG pipeline like a distributed system, you can log &lt;strong&gt;structured spans&lt;/strong&gt; (retrieval latency, reranker latency, generation latency) without storing raw content. When you need content occasionally, capture it on a sampling path with explicit approvals.&lt;/p&gt;

&lt;p&gt;If you’re already working on evals, tie auditability to your regression gates. My approach in &lt;a href="https://dev.to/blog/ai-engineering-evals-gates"&gt;AI Engineering Evals: Regression Gates for Prompts, Tools, RAG [2026]&lt;/a&gt; is to treat “privacy regressions” like correctness regressions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Encryption and access controls for vector databases and logs
&lt;/h2&gt;

&lt;p&gt;Most “RAG privacy” advice stops at redaction. That’s incomplete.&lt;/p&gt;

&lt;p&gt;Even perfectly redacted systems fail if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anyone in the org can query the vector DB,&lt;/li&gt;
&lt;li&gt;logs are accessible to the whole engineering org,&lt;/li&gt;
&lt;li&gt;service-to-service permissions are broad.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vector database security: the basics that still get missed
&lt;/h3&gt;

&lt;p&gt;For your vector database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt at rest&lt;/strong&gt; (KMS-managed keys, ideally per environment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt in transit&lt;/strong&gt; (mTLS between services)&lt;/li&gt;
&lt;li&gt;Enforce &lt;strong&gt;RBAC&lt;/strong&gt;. Retrieval service accounts should not have admin permissions.&lt;/li&gt;
&lt;li&gt;Enforce &lt;strong&gt;tenant isolation&lt;/strong&gt;. Separate indexes per tenant or mandatory tenant filters enforced server-side.&lt;/li&gt;
&lt;li&gt;Treat metadata like a boundary. A doc ID can be sensitive if it maps to a customer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re using Postgres with &lt;code&gt;pgvector&lt;/code&gt;, lean on Postgres RBAC and row-level security. If you’re using a managed vector DB, verify that RBAC is enforced at query time, not just in dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs and traces: least privilege + “break glass” access
&lt;/h3&gt;

&lt;p&gt;Your logging platform is often broader-access than your databases. That’s backwards.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default debug traces to &lt;strong&gt;restricted access&lt;/strong&gt; (on-call, security, a small number of engineers).&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;break-glass&lt;/strong&gt; workflows: temporary access with approvals, fully audited.&lt;/li&gt;
&lt;li&gt;Separate environments. Production traces shouldn’t be casually accessible from dev accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the Walmart chatbot system, we leaned heavily on event streaming for context pipeline latency. Kafka made the pipeline fast. It also creates another place to leak data if you don’t treat topics as sensitive. Partition topics by sensitivity, encrypt payloads where needed, and don’t let “observability” topics become an ungoverned dump.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing and monitoring: validating redaction and catching regressions
&lt;/h2&gt;

&lt;p&gt;You can’t “promise” data privacy. You can only prove it continuously.&lt;/p&gt;

&lt;p&gt;Here’s what works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build redaction fixtures and run them in CI
&lt;/h3&gt;

&lt;p&gt;Create a small corpus of synthetic documents and user queries that contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple PII types (email, phone, address)&lt;/li&gt;
&lt;li&gt;PHI patterns (patient ID, diagnosis codes)&lt;/li&gt;
&lt;li&gt;PCI patterns (test card numbers)&lt;/li&gt;
&lt;li&gt;secrets (fake API keys, JWT-like tokens)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then test the pipeline at multiple points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;after ingestion redaction&lt;/li&gt;
&lt;li&gt;after retrieval filtering&lt;/li&gt;
&lt;li&gt;after prompt assembly&lt;/li&gt;
&lt;li&gt;in the logging output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same mindset as prompt regression testing. You’re not “hoping” you didn’t leak. You’re gating releases on it.&lt;/p&gt;

&lt;p&gt;If you want a broader framework for these gates, &lt;a href="https://dev.to/blog/evaluate-ai-agents-production-testing"&gt;Evaluate AI Agents in Production: 2026 Testing Guide&lt;/a&gt; is a good complement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor with metrics, not vibes
&lt;/h3&gt;

&lt;p&gt;You should have metrics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;redaction_hit_rate&lt;/strong&gt;: % of requests where any redaction occurred&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;secret_block_rate&lt;/strong&gt;: % blocked due to secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pii_suspected_rate&lt;/strong&gt;: sampled detection on outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unredacted_span_rate&lt;/strong&gt;: traces that contain raw content fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick a baseline and alert on regressions. Even a 0.1% leak rate is catastrophic at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sampling and review: the human layer that still matters
&lt;/h3&gt;

&lt;p&gt;Automated detection misses edge cases. Use sampling, but do it safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sample &lt;strong&gt;redacted&lt;/strong&gt; traces by default&lt;/li&gt;
&lt;li&gt;allow short-lived access to raw content only with approvals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, “privacy-preserving tracing” is the compromise that works. You debug latency and retrieval behavior with IDs and hashes. You pull raw content only on an incident path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Incident response: how audit trails help investigate leaks
&lt;/h2&gt;

&lt;p&gt;When something leaks, the worst moment to discover your logging strategy is when you’re already in incident mode and someone asks: “So… what exactly got stored?”&lt;/p&gt;

&lt;p&gt;A strong audit trail lets you answer quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tenant was affected?&lt;/li&gt;
&lt;li&gt;Which user role queried it?&lt;/li&gt;
&lt;li&gt;Which documents were retrieved?&lt;/li&gt;
&lt;li&gt;Which policy version allowed it?&lt;/li&gt;
&lt;li&gt;Did redaction run?&lt;/li&gt;
&lt;li&gt;Which model version generated the response?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A practical incident workflow
&lt;/h3&gt;

&lt;p&gt;Here’s a workflow I like because it respects both privacy and reality:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contain&lt;/strong&gt;: disable high-risk features (long-context retrieval, tool calls) for affected tenant(s).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlate&lt;/strong&gt;: use &lt;code&gt;request_id&lt;/code&gt; to trace the retrieval chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt;: pull the exact retrieved document IDs and chunk IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce&lt;/strong&gt;: replay using hashes and template versions, not the raw prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remediate&lt;/strong&gt;: fix the policy or ingestion redaction, re-index, rotate secrets if applicable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove&lt;/strong&gt;: write the incident report referencing audit events, not raw user data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you don’t have structured audit events, you’ll fall back to grepping logs for raw prompts. That’s slow, it’s messy, and it tends to create a second leak while investigating the first.&lt;/p&gt;

&lt;p&gt;For broader security hardening beyond privacy, pair this with &lt;a href="https://dev.to/blog/ai-security-complete-guide"&gt;The Complete Guide to AI Security in 2026&lt;/a&gt; and &lt;a href="https://dev.to/pillars/ai-security"&gt;AI security&lt;/a&gt; work.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical end-to-end architecture (where to put the controls)
&lt;/h2&gt;

&lt;p&gt;If you want a concrete system picture, here’s the enforcement map I recommend for production AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controls by boundary
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client SDK / UI&lt;/strong&gt;: detect obvious secrets before they leave the device; warn users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API gateway&lt;/strong&gt;: rate-limit, tenant auth, basic PII/secret scanning, request IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion pipeline&lt;/strong&gt;: heavy redaction + classification + manifests; store redacted RAG copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retriever service&lt;/strong&gt;: tenant partitioning, ACL checks, policy filters, denylist/allowlist rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt builder&lt;/strong&gt;: last-mile redaction; prompt injection defenses; template versioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM provider&lt;/strong&gt;: use no-training/no-retention modes where available, but don’t pretend this is your main control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: split compliance audit events from debug traces; TTL and access controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also how you keep your &lt;a href="https://dev.to/pillars/ai-engineering-production"&gt;production AI&lt;/a&gt; posture sane. When everything becomes “just one more JSON field in the trace,” you will ship a privacy regression.&lt;/p&gt;

&lt;h3&gt;
  
  
  The policy-as-code approach (2026 reality)
&lt;/h3&gt;

&lt;p&gt;You want policies that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;versioned&lt;/li&gt;
&lt;li&gt;testable&lt;/li&gt;
&lt;li&gt;deployable independently&lt;/li&gt;
&lt;li&gt;referenced in audit events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same discipline you apply to CI/CD.&lt;/p&gt;

&lt;p&gt;If you’re already thinking in terms of control flow and gates, &lt;a href="https://dev.to/blog/ai-agent-control-flow-patterns"&gt;AI Agent Control Flow Patterns [2026]: Retries, HITL, Checkpoints&lt;/a&gt; has the same spirit. The system is the product, not the prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: the prediction
&lt;/h2&gt;

&lt;p&gt;RAG is becoming the default “enterprise LLM” architecture. That also means RAG is becoming the default enterprise privacy failure mode.&lt;/p&gt;

&lt;p&gt;My prediction: by late 2026, the teams that win audits won’t be the ones with the fanciest redaction model. They’ll be the ones who can point to a retention matrix, a policy version, and a tamper-evident audit event stream, and say: “Here’s exactly what we store, for how long, and why.”&lt;/p&gt;

&lt;p&gt;If you’re building RAG today, stop treating data privacy like a bolt-on. Design it the way you design latency budgets. End-to-end. Measurable. Enforced at every boundary.&lt;/p&gt;

&lt;p&gt;Want a challenge? Pick one artifact from the retention matrix above and actually implement TTL + deletion + audit proof for it this week. That’s the difference between a demo and a system you can defend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.kunalganglani.com/blog/data-privacy-rag-redaction-retention?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=data-privacy-rag-redaction-retention" rel="noopener noreferrer"&gt;kunalganglani.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>privacy</category>
      <category>llmops</category>
      <category>datasecurity</category>
    </item>
  </channel>
</rss>
