<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Learning in Public</title>
    <subtitle>Julian Bonomini&#x27;s notes on systems programming, tooling and whatever side quest he&#x27;s on, wrong turns included.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://buenomini.com/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://buenomini.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-10-12T00:00:00+00:00</updated>
    <id>https://buenomini.com/atom.xml</id>
    <entry xml:lang="en">
        <title>Where&#x2F;what is my value as an engineer today?</title>
        <published>2026-10-12T00:00:00+00:00</published>
        <updated>2026-10-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/26-wheres-my-value-as-an-engineer/"/>
        <id>https://buenomini.com/posts/26-wheres-my-value-as-an-engineer/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/26-wheres-my-value-as-an-engineer/">&lt;p&gt;There are two camps right now on engineers using agents. One says agents can already do everything, you just define what you want, they build it, you manage them and review what comes back. Human attention is the bottleneck (as we are probably tired of hearing), so put yours on what to build and let the agents figure out the how. The other camp doesn&#x27;t trust any of it.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t think we have a norm for responsible AI usage in engineering yet. You&#x27;re either balls deep or very skeptical, and there isn&#x27;t much in between.&lt;&#x2F;p&gt;
&lt;p&gt;The closest thing to a norm I&#x27;ve seen is Mitchell Hashimoto&#x27;s &quot;whiteboard defense&quot;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;I should be able to pull you aside at any moment and ask you to explain any customer-facing system you&#x27;ve shipped. You should be able to clearly explain how it works and defend the decisions you made. This is my benchmark for responsible AI usage.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;He doesn&#x27;t expect line-level familiarity, you don&#x27;t have to remember the function name. But if he asks &quot;why did you do X instead of Y?&quot; or &quot;where does this fail?&quot; you should be able to answer.&lt;&#x2F;p&gt;
&lt;p&gt;I fully agree with that. My value as an engineer now is a deep understanding of what is happening and why. I don&#x27;t need to remember the method name, the variable name or the file. The agent probably writes better code than me line by line anyway. But for me that&#x27;s the floor.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;owning-the-shape&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#owning-the-shape&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Owning the shape&lt;&#x2F;h2&gt;
&lt;p&gt;A lot of people say engineers become product people now. I think there&#x27;s still a clear distinction. Product is the what, and many times the why. Engineering is the how, but also the why, and the why goes hand in hand with the how. Why this module owns that state, why that thing sits behind an interface.&lt;&#x2F;p&gt;
&lt;p&gt;When the agent starts deciding the shape of the code (how big things are, where they live, how they connect) and it doesn&#x27;t line up with what I would have done, I&#x27;m not engineering it anymore. I&#x27;m just product owning it.&lt;&#x2F;p&gt;
&lt;p&gt;And you can pass the whiteboard test the whole time. You read what the agent built, you understand it, you can explain it to anyone who pulls you aside, and you&#x27;re still stuck with a shape you never chose.&lt;&#x2F;p&gt;
&lt;p&gt;I like my modules deep, a lot of behaviour behind a small interface. I wrote my own skills describing how I want codebases designed, and it works, somewhat. It never gets it fully right.&lt;&#x2F;p&gt;
&lt;p&gt;Agents are very knowledgeable but lazy. They take shortcuts, a bolted-on fix here, a special case there, and each one looks fine on its own. I&#x27;ve let them run wild, then wanted to make a change later on and had to refactor almost the entire codebase because of one wrong turn the agent took that I wouldn&#x27;t have taken.&lt;&#x2F;p&gt;
&lt;p&gt;Every project already has constraints, from scratch or established. I don&#x27;t want another one that only exists because of whatever the agent happened to write first. Building things that are extensible and easy to pivot without making them overkill is a judgment call, and agents don&#x27;t have that judgment yet. Once I lose the how and the why, every incremental change and every pivot gets harder.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-spaghetti-argument&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-spaghetti-argument&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The spaghetti argument&lt;&#x2F;h2&gt;
&lt;p&gt;The strongest pushback I know goes like this: &quot;If agents are the ones reading the code, who cares if the modules are deep or it&#x27;s spaghetti? Tests pass (I also have opinions around ai gen tests), it works, and when it gets messy an agent refactors it in an afternoon.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s true if you could trust them blindly. I don&#x27;t think we&#x27;re there, and maybe we won&#x27;t be for a while. Human eval is still needed, at least for now.&lt;&#x2F;p&gt;
&lt;p&gt;And I&#x27;m still the one responsible for what I ship. I own the engineering side and the codebase, so I should be able to jump in and apply what I know line by line if needed. That&#x27;s a lot harder in a codebase shaped by someone else&#x27;s shortcuts. And agents get way more useful when you understand what&#x27;s happening at a deep level anyway.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-i-don-t-care&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#where-i-don-t-care&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Where I don&#x27;t care&lt;&#x2F;h2&gt;
&lt;p&gt;Ephemeral work is different. A script to do something once, a prototype, a quick automation to make a process easier one time. I don&#x27;t care how it&#x27;s done, I just need it to work, so I fire and forget. Hashimoto draws the same line: &quot;For PoCs, demos, experiments, whatever: I don&#x27;t care. Generate 100% of it and understand none of it.&quot;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;if-you-re-all-in&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#if-you-re-all-in&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
If you&#x27;re all in&lt;&#x2F;h2&gt;
&lt;p&gt;Slow down a bit. Look at the code, at least at its shape. Decide where things go before the agent does, write it down somewhere it will read, and step in when it bolts something on. Being AFK on the how is fine for a while, until it is not.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;if-you-re-skeptical&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#if-you-re-skeptical&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
If you&#x27;re skeptical&lt;&#x2F;h2&gt;
&lt;p&gt;Speed up. Let them type and keep the design yours, and for ephemeral stuff don&#x27;t even look at the code.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-this-goes&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#where-this-goes&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Where this goes&lt;&#x2F;h2&gt;
&lt;p&gt;I don&#x27;t know what happens as models get better. Maybe they get the shape right and this post ages badly. Right now there&#x27;s a gap between how an agent shapes a codebase and how I would, and that gap is where my value is.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t think agents are replacing us yet, but you never know, so adapting is key.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Branding a primitive</title>
        <published>2026-09-04T00:00:00+00:00</published>
        <updated>2026-09-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/25-brand-discovery-for-a-protocol/"/>
        <id>https://buenomini.com/posts/25-brand-discovery-for-a-protocol/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/25-brand-discovery-for-a-protocol/">&lt;p&gt;TrueSeal is the E2EE sync primitive I keep mentioning. It&#x27;s a library and a relay, with no UI and no users of its own. Branding for that sounded like fluff to me. A primitive needs good docs and a clean API, and branding is for apps people look at.&lt;&#x2F;p&gt;
&lt;p&gt;But my job with this project is to put something out and convince people to use it. Nobody installs a sync library because it exists. A dev has to land on the home page, believe it solves their problem and trust it with their user data, and all of that happens before they ever see the API. Deciding what that page says, and how it sounds, is branding. So a primitive needs it as much as an app does.&lt;&#x2F;p&gt;
&lt;p&gt;When I needed to start working on the docs site, I tried one shoting a few prototypes with an agent. I had four home page variants built and I didn&#x27;t love any of them.&lt;&#x2F;p&gt;
&lt;p&gt;A monochrome one was too boring, I didn&#x27;t know where to look. The colorful one had rounded cards and gradients, which didn&#x27;t fit a building block. I figured the problem was visual, so I did a proper brand discovery: purpose, positioning, audience, personality, voice, and only then the look.&lt;&#x2F;p&gt;
&lt;p&gt;Most of what came out of it had nothing to do with colors, and that&#x27;s the part a primitive needs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;positioning-the-competitor-is-giving-up&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#positioning-the-competitor-is-giving-up&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Positioning: the competitor is giving up&lt;&#x2F;h2&gt;
&lt;p&gt;One of the questions was who TrueSeal competes with. I would&#x27;ve expected product names to be the answer, but the main reason for TrueSeal to exist was that most devs bolt encryption onto whatever sync stack they already have, which sounds like the answer. But when I thought about what I&#x27;d actually tell a dev halfway through that, it was &quot;Don&#x27;t give up on privacy. Use this, it&#x27;s free&quot; (free in effort, mostly).&lt;&#x2F;p&gt;
&lt;p&gt;Because that&#x27;s the real fear. Pairing, key sharing, recovery... if you have to build all of that yourself it&#x27;s a lot of work, so most people won&#x27;t. They&#x27;ll ship Firestore unencrypted and move on. That&#x27;s the thing I&#x27;m up against, a perfectly reasonable dev who settles.&lt;&#x2F;p&gt;
&lt;p&gt;So the pitch is that privacy shouldn&#x27;t be a project you take on. In my head the dev lists what their app does and says &quot;and it is fully private&quot; at the end, almost as an afterthought.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;personality-no-trust-us-no-i-know-better&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#personality-no-trust-us-no-i-know-better&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Personality: no &quot;trust us&quot;, no &quot;I know better&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;A couple of the candidate lines leaned on &quot;don&#x27;t trust anyone&quot;. Which is kind of my whole temperament, I just don&#x27;t like &quot;trust us&quot;, it&#x27;s not good enough for me. But I can&#x27;t tell you to trust no one and in the same breath ask you to trust me. So that whole tone went out. Same with the more rebellious personality options, they all sounded like &quot;I know better&quot;, and I don&#x27;t want this thing to ever sound like that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-look-came-last&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-look-came-last&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The look came last&lt;&#x2F;h2&gt;
&lt;p&gt;The stuff I already liked in the prototypes, square corners and the blinking cursor, finally had a reason behind it. And the first home page built on top of the brandbook was the first one I said &quot;I love it&quot; to.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hiding IPs from my own relay is harder than it sounds</title>
        <published>2026-08-28T00:00:00+00:00</published>
        <updated>2026-08-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/24-hiding-ips-from-my-relay/"/>
        <id>https://buenomini.com/posts/24-hiding-ips-from-my-relay/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/24-hiding-ips-from-my-relay/">&lt;p&gt;While I was writing down what TrueSeal promises and I called it &quot;private and anonymous&quot; (something I like to do detached from implementing stuff, to make myself accountable) I realised that the threat model I&#x27;d already accepted said that the relay sees IP addresses and timing, and push fan-out lets it guess who shares a group. So anonymous was out not really true unless you mask your own I not really true unless you mask your own IP.&lt;&#x2F;p&gt;
&lt;p&gt;Pretty simple tho, I&#x27;ll change the relay to never see IPs.&lt;&#x2F;p&gt;
&lt;p&gt;The relay software already never reads, stores, logs or keys anything on an IP. But a TCP connection hands the source address to whoever hosts the thing. The host OS sees it, the VPS provider sees it, and so does any reverse proxy you put in front. So &quot;never see IPs&quot; really means adding a network hop the relay host doesn&#x27;t control.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-options&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-options&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The options&lt;&#x2F;h2&gt;
&lt;p&gt;My first idea was a proxy in front of the relay, and it&#x27;s useless against the one party I care about. The proxy sees the IP, the relay sees the traffic, and the same person holds both logs. The &lt;a href=&quot;https:&#x2F;&#x2F;www.rfc-editor.org&#x2F;rfc&#x2F;rfc9458&quot;&gt;Oblivious HTTP RFC (9458)&lt;&#x2F;a&gt; says the two sides can&#x27;t be run by the same entity. With a self-hosted relay, &quot;the operator&quot; is whoever runs the Docker compose, so the hop has to live outside their hands.&lt;&#x2F;p&gt;
&lt;p&gt;A Tor onion service works. The relay host never learns client IPs, and Noise runs unchanged inside the Tor stream. But that is a bigger more expensive change:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;About 430 ms median to get a first response from an onion service, on Tor&#x27;s own desktop measurements, and that&#x27;s without the time Tor takes to boot.&lt;&#x2F;li&gt;
&lt;li&gt;Every self-hoster runs a Tor daemon next to the relay and keeps a second secret key around.&lt;&#x2F;li&gt;
&lt;li&gt;iOS can take the socket away when the app is suspended, and a background push gives you 30 seconds. So the phone has to boot Tor again and reach the onion service inside that window. idk, for a library that&#x27;s supposed to be &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;19-docker-bug-debug-session-trueseal&#x2F;&quot;&gt;boring on purpose&lt;&#x2F;a&gt; that sounds like a lot of new ways to break.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;A proxy hop run by someone else (how iCloud Private Relay works) is the cheapest fit. About one extra round trip, and iOS 17+ supports it natively. It only helps if an independent operator exists and doesn&#x27;t collude with the relay, and I don&#x27;t have one.&lt;&#x2F;p&gt;
&lt;p&gt;Oblivious HTTP doesn&#x27;t fit at all. It&#x27;s built for requests that carry no state between them, and TrueSeal keeps a long-lived receive session open.&lt;&#x2F;p&gt;
&lt;p&gt;Nym is the only option that also hides timing. It&#x27;s heavy and poor on mobile tho.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-own-code-was-leaking-too&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#my-own-code-was-leaking-too&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
My own code was leaking too&lt;&#x2F;h2&gt;
&lt;p&gt;A device&#x27;s receiving connection and its sending connections come from the same IP. The push session uses Noise NK on purpose so the relay doesn&#x27;t learn who sent a message. But if the host sees the same IP on both, it can re-identify the sender anyway. That goes into the threat model whatever I decide about Tor.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-i-did-instead-for-now&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-i-did-instead-for-now&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What I did instead (for now)&lt;&#x2F;h2&gt;
&lt;p&gt;For now I changed the docs so they don&#x27;t lie. They now say: &quot;The relay never logs, stores or uses your IP address. The server it runs on still sees the connection, as with any internet service. To hide your IP from the server too, use a VPN or Tor.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;&quot;Doesn&#x27;t record or use&quot; and &quot;can&#x27;t see&quot; sound almost the same to a user, and they&#x27;re very different promises.&lt;&#x2F;p&gt;
&lt;p&gt;&quot;Can&#x27;t see your IP&quot; is banned from all public copy, along with &quot;zero-knowledge&quot;. And the claim has a test behind it: an e2e scenario runs the real relay in normal and dev mode and fails if any client address shows up in the logs or in &lt;code&gt;inbox.db&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Real IP hiding is out of the preview, and not required for 1.0 either. What I did keep is one place in the client that opens sockets, so Tor or a proxy hop can go in later without touching anything else.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m also thinking about making the sync library refuse to talk to a relay that isn&#x27;t running my code. It stays self-hostable, but if someone modifies the relay, the sync won&#x27;t talk to it. (via checksum or something like that, have not investigated these type of mechanisms yet)&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TrueSeal delivery guarantees</title>
        <published>2026-08-14T00:00:00+00:00</published>
        <updated>2026-08-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/23-delivery-guarantees/"/>
        <id>https://buenomini.com/posts/23-delivery-guarantees/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/23-delivery-guarantees/">&lt;p&gt;The TrueSeal docs used to say this about delivery: &quot;Every blob reaches every group member. Eventually, unconditionally.&quot; That was my promise, and no sync library can keep that promise, mine included.&lt;&#x2F;p&gt;
&lt;p&gt;TrueSeal is the E2EE sync thing I&#x27;ve been building. A device sends an encrypted message, the relay holds it for each recipient, the recipient picks it up and tells the relay &quot;got it, you can delete it&quot;. That &quot;got it&quot; is the ack.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;at-least-once-in-plain-words&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#at-least-once-in-plain-words&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
At-least-once in plain words&lt;&#x2F;h2&gt;
&lt;p&gt;The sender keeps retrying until the relay says it stored the message. The relay keeps the message until the recipient acks it. If the ack gets lost (connection drops, app gets killed), the relay doesn&#x27;t know the message arrived, so it sends it again on the next connection. Nothing gets dropped, but the same message can show up twice. That&#x27;s at-least-once.&lt;&#x2F;p&gt;
&lt;p&gt;Exactly-once would mean the network and the app&#x27;s database commit together, atomically, and they can&#x27;t. There&#x27;s always a moment between &quot;the app stored it&quot; and &quot;the relay heard about it&quot; where a crash puts you on one side or the other. You pick which side: maybe lose it, or maybe get it twice.&lt;&#x2F;p&gt;
&lt;p&gt;And my setup had the worst version of the lose-it side. The device acked as soon as the bytes arrived, before decryptinge. That was on purpose, acking only on success would tell the relay whether decryption worked. But it meant a crash right after the ack lost the message for good.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;first-fix-apps-dedup&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#first-fix-apps-dedup&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
First fix: apps dedup&lt;&#x2F;h2&gt;
&lt;p&gt;Someone has to drop the duplicates. An old ADR of mine already said &quot;callers dedup&quot;, but the SDKs dropped the sequence number, so no app could. The protocol &lt;em&gt;has to stay agnostic about payloads&lt;&#x2F;em&gt;, so I went with &quot;the app owns it&quot;. The library now exposes a stable &lt;code&gt;messageId&lt;&#x2F;code&gt; (derived from the sender plus its sequence number), and every app stores processed IDs in the same transaction as its own data and skips the repeats.&lt;&#x2F;p&gt;
&lt;p&gt;That shipped in every SDK, and the docs got rewritten to say &quot;at-least-once&quot; (and to stop telling people to dedup by content hash, which is wrong, two deliberate sends can carry the same bytes).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;less-than-two-weeks-later&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#less-than-two-weeks-later&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Less than two weeks later&lt;&#x2F;h2&gt;
&lt;p&gt;Then I went through the whole delivery contract again, and my one note on it was &quot;these developer&#x2F;engineer-first primitives need to be as simple as possible to implement&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;Apply that to dedup and the answer flips. &quot;Every app must store processed IDs atomically with its own data&quot; is easy to get wrong, and every app would have to get it right on its own. And the agnostic argument doesn&#x27;t hold up, the library doesn&#x27;t need to understand the payload to remember an ID it already handled. So now:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The app&#x27;s message handler is async, and the library waits for it to finish.&lt;&#x2F;li&gt;
&lt;li&gt;The library writes the message ID to its own dedup table (kept 60 days).&lt;&#x2F;li&gt;
&lt;li&gt;Only then does it ack the relay.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;

&lt;figure class=&quot;inline mermaid-fig&quot;&gt;&lt;pre class=&quot;mermaid&quot;&gt;sequenceDiagram
    participant R as Relay
    participant L as Library
    participant A as App handler
    R-&amp;gt;&amp;gt;L: deliver message
    L-&amp;gt;&amp;gt;L: decrypt, verify, check dedup table
    L-&amp;gt;&amp;gt;A: run handler
    Note over A: crash here, relay redelivers and handler runs again
    A--&amp;gt;&amp;gt;L: done
    L-&amp;gt;&amp;gt;L: write message ID
    Note over L: crash here, redelivered but skipped as duplicate
    L-&amp;gt;&amp;gt;R: ack&lt;&#x2F;pre&gt;&lt;&#x2F;figure&gt;
&lt;script type=&quot;module&quot;&gt;
  import mermaid from &quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;npm&#x2F;mermaid@11&#x2F;dist&#x2F;mermaid.esm.min.mjs&quot;;
  const dark = document.documentElement.classList.contains(&quot;dark&quot;);
  mermaid.initialize({ startOnLoad: true, theme: dark ? &quot;dark&quot; : &quot;neutral&quot;, fontFamily: &quot;Mona Sans, system-ui, sans-serif&quot; });
&lt;&#x2F;script&gt;
&lt;p&gt;The library still acks messages it can&#x27;t use (bad signature, not a member), so the relay still can&#x27;t tell if decryption worked. All it sees is how long processing took.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s still not exactly-once. The promise is now &quot;a handler runs twice for the same message only if the app crashed while handling it&quot;. So handlers should still be idempotent, but the common duplicates (lost acks, sender replays) never reach app code.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Membership without a server</title>
        <published>2026-08-06T00:00:00+00:00</published>
        <updated>2026-08-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/22-membership-without-a-server/"/>
        <id>https://buenomini.com/posts/22-membership-without-a-server/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/22-membership-without-a-server/">&lt;p&gt;In trueseal-sync a group is a bunch of devices sharing a relay. The relay stores and forwards. It can&#x27;t read anything and it has no say in who&#x27;s in the group.&lt;&#x2F;p&gt;
&lt;p&gt;Membership lives in a signed Group Manifest, which is a version number, the list of members, who issued it and a signature. Any member can add or remove someone by issuing a new one with a higher version, and the highest version wins.&lt;&#x2F;p&gt;
&lt;p&gt;The first version had three problems:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Equal versions forked. If two devices issued version 5 at the same time, each device kept whichever one it saw first and rejected the other as a &quot;regression&quot;. Nothing linked a manifest to the one before it, so one of the two edits just vanished (removals too) and nobody got told.&lt;&#x2F;li&gt;
&lt;li&gt;Removed devices could still rewrite membership. The only check was &quot;is the issuer in my current manifest&quot;, so a device that hadn&#x27;t seen the removal yet would take a high-version manifest from the removed device and then reject the real removal.&lt;&#x2F;li&gt;
&lt;li&gt;Anyone could freeze the group forever. Any member could issue version &lt;code&gt;u64::MAX&lt;&#x2F;code&gt;, and then &lt;code&gt;version + 1&lt;&#x2F;code&gt; overflows (panic in debug, wrap in release).&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;what-i-picked&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-i-picked&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What I picked&lt;&#x2F;h2&gt;
&lt;p&gt;Any member can change membership (no admins), every current member is fully trusted for it, and the answer to a hostile device is to destroy the group. Max group size is 32.&lt;&#x2F;p&gt;
&lt;p&gt;Then the harder one, two devices editing at the same time. Every manifest now carries &lt;code&gt;parent&lt;&#x2F;code&gt;, the SHA-256 hash of the manifest it was built on, covered by the signature. Devices order manifests by &lt;code&gt;(version, hash)&lt;&#x2F;code&gt;. On a tie, the lower hash wins. Any device that sees both manifests picks the same one, so there&#x27;s no fork anymore.&lt;&#x2F;p&gt;
&lt;p&gt;That alone still loses the other edit, though. So the issuer keeps its change as a Pending Membership Change, and if it accepts a concurrent manifest that doesn&#x27;t include it, it re-applies the change on top as &lt;code&gt;version + 1&lt;&#x2F;code&gt;. The retry has to be the issuer&#x27;s job, because it&#x27;s the only device that knows its change lost. The parent link is what tells &quot;someone undid my change on purpose&quot; (built on a manifest that had it) apart from &quot;we raced&quot; (built on something older).&lt;&#x2F;p&gt;
&lt;figure class=&quot;inline has-port&quot;&gt;
  &lt;svg class=&quot;fig fig-land&quot; viewBox=&quot;0 0 760 300&quot; role=&quot;img&quot; aria-label=&quot;Manifest chain: version 3 has two children at version 4, one from device A removing D and one from device C admitting E. Both name version 3 as their parent. A&#x27;s manifest has the lower hash, so every device keeps it. C&#x27;s change is still pending, so C re-applies it as version 5 with A&#x27;s manifest as its parent, and the group ends with A, B, C and E.&quot;&gt;&lt;defs&gt;&lt;marker id=&quot;ah-mfork-land&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path class=&quot;arrow&quot; d=&quot;M0 0 L10 5 L0 10 z&quot;&#x2F;&gt;&lt;&#x2F;marker&gt;&lt;marker id=&quot;ah-mfork-land-dim&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path class=&quot;arrow-dim&quot; d=&quot;M0 0 L10 5 L0 10 z&quot;&#x2F;&gt;&lt;&#x2F;marker&gt;&lt;&#x2F;defs&gt;&lt;path class=&quot;edge&quot; d=&quot;M190 135 L255 80&quot; marker-end=&quot;url(#ah-mfork-land)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;200&quot; y=&quot;96&quot; text-anchor=&quot;middle&quot;&gt;parent&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M190 165 L255 220&quot; marker-end=&quot;url(#ah-mfork-land-dim)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;200&quot; y=&quot;212&quot; text-anchor=&quot;middle&quot;&gt;parent&lt;&#x2F;text&gt;&lt;path class=&quot;edge&quot; d=&quot;M455 70 L535 70&quot; marker-end=&quot;url(#ah-mfork-land)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;495&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot;&gt;parent&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M455 230 L620 230 L620 112&quot; marker-end=&quot;url(#ah-mfork-land-dim)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;560&quot; y=&quot;250&quot; text-anchor=&quot;middle&quot;&gt;still pending, re-apply&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;20&quot; y=&quot;110&quot; width=&quot;170&quot; height=&quot;80&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;34&quot; y=&quot;138&quot;&gt;v3&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;34&quot; y=&quot;160&quot;&gt;A B C D&lt;&#x2F;text&gt;&lt;rect class=&quot;box relay&quot; x=&quot;255&quot; y=&quot;30&quot; width=&quot;200&quot; height=&quot;80&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;269&quot; y=&quot;58&quot;&gt;v4 from A&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;269&quot; y=&quot;78&quot;&gt;remove D&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;269&quot; y=&quot;97&quot;&gt;lower hash, kept&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;255&quot; y=&quot;190&quot; width=&quot;200&quot; height=&quot;80&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;269&quot; y=&quot;218&quot;&gt;v4 from C&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;269&quot; y=&quot;238&quot;&gt;admit E&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;269&quot; y=&quot;257&quot;&gt;higher hash, dropped&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;535&quot; y=&quot;30&quot; width=&quot;205&quot; height=&quot;80&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;549&quot; y=&quot;58&quot;&gt;v5 from C&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;549&quot; y=&quot;78&quot;&gt;admit E again&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;549&quot; y=&quot;97&quot;&gt;A B C E&lt;&#x2F;text&gt;&lt;&#x2F;svg&gt;
&lt;svg class=&quot;fig fig-port&quot; viewBox=&quot;0 0 400 520&quot; role=&quot;img&quot; aria-label=&quot;Manifest chain: version 3 has two children at version 4, one from device A removing D and one from device C admitting E. Both name version 3 as their parent. A&#x27;s manifest has the lower hash, so every device keeps it. C&#x27;s change is still pending, so C re-applies it as version 5 with A&#x27;s manifest as its parent, and the group ends with A, B, C and E.&quot;&gt;&lt;defs&gt;&lt;marker id=&quot;ah-mfork-port&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path class=&quot;arrow&quot; d=&quot;M0 0 L10 5 L0 10 z&quot;&#x2F;&gt;&lt;&#x2F;marker&gt;&lt;marker id=&quot;ah-mfork-port-dim&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path class=&quot;arrow-dim&quot; d=&quot;M0 0 L10 5 L0 10 z&quot;&#x2F;&gt;&lt;&#x2F;marker&gt;&lt;&#x2F;defs&gt;&lt;path class=&quot;edge&quot; d=&quot;M150 100 L105 170&quot; marker-end=&quot;url(#ah-mfork-port)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;100&quot; y=&quot;138&quot; text-anchor=&quot;middle&quot;&gt;parent&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M250 100 L295 170&quot; marker-end=&quot;url(#ah-mfork-port-dim)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;302&quot; y=&quot;138&quot; text-anchor=&quot;middle&quot;&gt;parent&lt;&#x2F;text&gt;&lt;path class=&quot;edge&quot; d=&quot;M105 270 L105 380&quot; marker-end=&quot;url(#ah-mfork-port)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;80&quot; y=&quot;330&quot; text-anchor=&quot;middle&quot;&gt;parent&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M295 270 L295 330 L190 380&quot; marker-end=&quot;url(#ah-mfork-port-dim)&quot;&#x2F;&gt;&lt;text class=&quot;el&quot; x=&quot;300&quot; y=&quot;352&quot; text-anchor=&quot;middle&quot;&gt;re-apply&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;110&quot; y=&quot;20&quot; width=&quot;180&quot; height=&quot;80&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;124&quot; y=&quot;48&quot;&gt;v3&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;124&quot; y=&quot;70&quot;&gt;A B C D&lt;&#x2F;text&gt;&lt;rect class=&quot;box relay&quot; x=&quot;15&quot; y=&quot;170&quot; width=&quot;180&quot; height=&quot;100&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;29&quot; y=&quot;198&quot;&gt;v4 from A&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;29&quot; y=&quot;220&quot;&gt;remove D&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;29&quot; y=&quot;240&quot;&gt;lower hash&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;29&quot; y=&quot;258&quot;&gt;kept&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;205&quot; y=&quot;170&quot; width=&quot;180&quot; height=&quot;100&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;219&quot; y=&quot;198&quot;&gt;v4 from C&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;219&quot; y=&quot;220&quot;&gt;admit E&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;219&quot; y=&quot;240&quot;&gt;higher hash&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;219&quot; y=&quot;258&quot;&gt;dropped&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;15&quot; y=&quot;380&quot; width=&quot;250&quot; height=&quot;90&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;29&quot; y=&quot;408&quot;&gt;v5 from C&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;29&quot; y=&quot;430&quot;&gt;admit E again&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;29&quot; y=&quot;452&quot;&gt;A B C E&lt;&#x2F;text&gt;&lt;&#x2F;svg&gt;

  &lt;figcaption&gt;Figure 1. A removes D while C admits E, both on top of v3. The lower hash wins the tie and C re-applies its admission on top.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;The promise: once every member has received the same manifests, they all hold the same one, and no honest member&#x27;s change is lost unless that member got removed at the same time. The app does nothing.&lt;&#x2F;p&gt;
&lt;p&gt;I did consider telling the app &quot;your change was lost, redo it&quot;. Simpler for me, but then every app has to get it right, and my rule for this library is that the app shouldn&#x27;t need to do anything to be correct. Merging add&#x2F;remove operations on every receiver would also work, but that&#x27;s a manifest redesign and way too much for a preview.&lt;&#x2F;p&gt;
&lt;p&gt;Member events can flicker though. A device can see E join, leave and join again while things settle. It&#x27;s documented, and apps should just render the current list.&lt;&#x2F;p&gt;
&lt;p&gt;For the freeze, &lt;code&gt;u64::MAX&lt;&#x2F;code&gt; is rejected on receipt, issuing uses checked arithmetic, and any jump over 1024 versions fails with &lt;code&gt;VersionJumpTooLarge&lt;&#x2F;code&gt;. Honest issuers always step by one. A device that misses more than 1024 membership changes has to re-pair, and I&#x27;m fine with that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-zombie&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-zombie&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The zombie&lt;&#x2F;h2&gt;
&lt;p&gt;A removed device wipes itself only if the manifest that excludes it was built on one it holds (or has held) that included it. Otherwise it&#x27;s a &quot;false removal&quot;, like a new joiner whose admission lost a tie, and it should stay alive and wait.&lt;&#x2F;p&gt;
&lt;p&gt;The &quot;has held&quot; part had a bug. When a device accepted a manifest and immediately re-applied a pending change on top, only the final manifest&#x27;s hash went into the held history. The one in the middle never got recorded, so a removal built on it looked like a false removal. No wipe, no removed event, and the device kept going with a stale view of a group it wasn&#x27;t in anymore (a zombie). The fix was to record both hashes.&lt;&#x2F;p&gt;
&lt;p&gt;One hole stays open on purpose. A removed device that refuses to cooperate can still mess with devices that haven&#x27;t seen its removal. Hardening that felt pointless when a hostile current member can already remove everyone, so it&#x27;s written down as the trust boundary.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Messages from the future</title>
        <published>2026-07-22T00:00:00+00:00</published>
        <updated>2026-07-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/21-error-messages-attack/"/>
        <id>https://buenomini.com/posts/21-error-messages-attack/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/21-error-messages-attack/">&lt;p&gt;I was deciding how trueseal-sync handles protocol versions since this is a big need for a sync protocol. Devices don&#x27;t upgrade at the same time. Someone updates the app on their phone, the laptop is still on the old one, and now the phone sends messages the laptop can&#x27;t read.&lt;&#x2F;p&gt;
&lt;p&gt;TrueSeal is store and forward. Device A pushes an encrypted blob to the relay, and the relay keeps it in device B&#x27;s inbox until B connects and acknowledges it. A can&#x27;t ask B which version it speaks, B might be offline for a week. So there&#x27;s no negotiation at all. Each device speaks one version, and the version is bound into the crypto (key derivation, AEAD associated data, signing input), which means a message from another version doesn&#x27;t even decrypt.&lt;&#x2F;p&gt;
&lt;p&gt;That leaves what B does with a message from a newer version than it knows. I saw three options:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Drop it. Acknowledge it so the relay deletes it, and move on. The person who updates late loses those messages.&lt;&#x2F;li&gt;
&lt;li&gt;Refuse it explicitly. From B&#x27;s side that&#x27;s dropping with an error attached, so the message is still gone.&lt;&#x2F;li&gt;
&lt;li&gt;Hold it. Don&#x27;t acknowledge it, so it stays on the relay. The relay resends the whole unacknowledged inbox on every connect until the messages expire, and once B upgrades they decrypt and nothing is lost.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Older or malformed messages get dropped, that one was easy. For newer ones I went with 3.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;anyone-can-send-version-99&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#anyone-can-send-version-99&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Anyone can send version 99&lt;&#x2F;h2&gt;
&lt;p&gt;Push in TrueSeal is unauthenticated on purpose. Anyone who knows a device&#x27;s public key can push to its inbox, and the relay doesn&#x27;t check who&#x27;s sending (to check that it would have to know who trusts who, and that&#x27;s the thing it shouldn&#x27;t know). Normally that&#x27;s fine because junk doesn&#x27;t decrypt, the device drops it, and the relay has quotas for floods.&lt;&#x2F;p&gt;
&lt;p&gt;A message from a newer version doesn&#x27;t decrypt either tho. B can&#x27;t check a format it doesn&#x27;t know, so junk labelled &quot;version 99&quot; looks the same as a real message from the future, and B keeps all of it, resent on every connect until it expires.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-errors-were-fine&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-errors-were-fine&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The errors were fine&lt;&#x2F;h2&gt;
&lt;p&gt;What got me looking at this area in the first place was errors. For unknown frame types the plan was that the relay answers with an explicit error, and my reply was &quot;only if it does not open an attack vector for being explicit at what is failing or something like that&quot;. I had padding oracles in the back of my head, where telling an attacker why something failed is all they need.&lt;&#x2F;p&gt;
&lt;p&gt;An oracle is an error the attacker can trigger over and over, where the answer depends on something secret (the padding of a plaintext they can&#x27;t read, in that case). None of these errors depend on anything secret. The relay&#x27;s error only goes back to the connection that sent the bad frame, inside its encrypted session. It&#x27;s a generic code with no echoed bytes, and the relay is open source, so &quot;unknown frame type&quot; is public knowledge anyway. A device never sends an error to another device. When it drops or holds something it raises an event that stays inside the app on that device, and whoever sent the message gets nothing back.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-cap&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#a-cap&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
A cap&lt;&#x2F;h2&gt;
&lt;p&gt;A device now holds at most 100 newer-version messages it can&#x27;t read&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-rust&quot;&gt;pub&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-rust&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-other z-rust&quot;&gt;MAX_HELD_BLOBS&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-rust&quot;&gt;usize&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-rust&quot;&gt;100&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Past that, the oldest held one gets acknowledged and dropped, and the app gets a &lt;code&gt;droppedWhileHeld&lt;&#x2F;code&gt; event so it knows. The relay&#x27;s per-inbox quotas (10,000 blobs or 256 MiB) are the second limit. A real late upgrader with more than 100 messages from the future loses the oldest ones. I&#x27;m ok with that, at least for the preview.&lt;&#x2F;p&gt;
&lt;p&gt;The relay can still see which messages a device leaves unacknowledged, so it can guess which version that device runs. I didn&#x27;t fix that one. It&#x27;s documented as a metadata limitation, next to the size and timing stuff the relay already sees.&lt;&#x2F;p&gt;
&lt;p&gt;Magic number? yep, but there is still too much to do and I always can come back to this.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Noise protocol its not concurrent</title>
        <published>2026-07-15T00:00:00+00:00</published>
        <updated>2026-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/20-relay-corrupting-stream/"/>
        <id>https://buenomini.com/posts/20-relay-corrupting-stream/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/20-relay-corrupting-stream/">&lt;p&gt;If you read the &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;19-docker-bug-debug-session-trueseal&#x2F;&quot;&gt;2 day debug session&lt;&#x2F;a&gt; post, you know how that one ended. The relay was an amd64 binary running under qemu on my Mac, the crypto came out wrong sometimes, I fixed the Dockerfile and wrote this commit message:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;bugfix: relay ran emulated → flynn&#x2F;noise crypto intermittently produced wrong AEAD output
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;I never had so much pain from a stupid error like this
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A while after that, I found the relay was corrupting its own encrypted stream, which could&#x27;ve been the actual real cause of the wrong AEAD output that I blamed on emulation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;two-goroutines-one-cipher-state&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#two-goroutines-one-cipher-state&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Two goroutines, one cipher state&lt;&#x2F;h2&gt;
&lt;p&gt;After the Noise handshake, the relay keeps a session open to each device, and two things write to it. A goroutine pushes new messages down (Deliver frames), and the read loop answers the client&#x27;s heartbeats. Both encrypt with the same outbound cipher state:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go z-code&quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-comment z-line z-go&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-go&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; deliver goroutine
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-variable z-declaration z-go&quot;&gt;encrypted&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-declaration z-go&quot;&gt;err&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-go&quot;&gt;:=&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-go&quot;&gt;cs2&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-dot z-go&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-function z-go&quot;&gt;Encrypt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-go&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-language z-go&quot;&gt;nil&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-go&quot;&gt;nil&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-go&quot;&gt;frame&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-go&quot;&gt;)&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-comment z-line z-go&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-go&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; ...
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-variable z-function z-go&quot;&gt;writeNoiseMsg&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-go&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-go&quot;&gt;conn&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-go&quot;&gt;encrypted&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-go&quot;&gt;)&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-comment z-line z-go&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-go&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; read loop, on a Heartbeat
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-variable z-declaration z-go&quot;&gt;hb&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-declaration z-go&quot;&gt;err&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-go&quot;&gt;:=&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-go&quot;&gt;cs2&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-dot z-go&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-function z-go&quot;&gt;Encrypt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-go&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-language z-go&quot;&gt;nil&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-go&quot;&gt;nil&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-function z-go&quot;&gt;Frame&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-go&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-go&quot;&gt;MsgTypeHeartbeat&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-go&quot;&gt;nil&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-go&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-go&quot;&gt;)&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-comment z-line z-go&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-go&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; ...
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-go&quot;&gt;&lt;span class=&quot;z-variable z-function z-go&quot;&gt;writeNoiseMsg&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-go&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-other z-go&quot;&gt;conn&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-go&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-go&quot;&gt;hb&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-go&quot;&gt;)&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Noise transport messages use a counter as the nonce. Every &lt;code&gt;Encrypt&lt;&#x2F;code&gt; bumps it, and the other side expects frames to arrive in exactly that order. flynn&#x2F;noise&#x27;s &lt;code&gt;CipherState&lt;&#x2F;code&gt; isn&#x27;t safe for concurrent use (did not know that), so with two goroutines calling it at once you can encrypt frame n and n+1 and put n+1 on the wire first. Then the client can&#x27;t decrypt and the session is dead.&lt;&#x2F;p&gt;
&lt;p&gt;And as a bonus, &lt;code&gt;writeNoiseMsg&lt;&#x2F;code&gt; wrote the 2-byte length and the body as two separate &lt;code&gt;Write&lt;&#x2F;code&gt; calls. Which means two writers could also interleave frames on the wire. It&#x27;s the same two-Write framing I stared at in theory 1 of the Docker post (I also wrote a whole post about &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;09-wire-protocol-framing&#x2F;&quot;&gt;framing&lt;&#x2F;a&gt; before that).&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t think the Docker post was wrong. That failure was on msg1 of the handshake, and this cipher state doesn&#x27;t even exist until the handshake is done, so the qemu thing was real. But this race was sitting there since the beginning of the receive session&lt;&#x2F;p&gt;
&lt;h2 id=&quot;one-writer&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#one-writer&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
One writer&lt;&#x2F;h2&gt;
&lt;p&gt;The fix is as simple as a &lt;code&gt;sessionWriter&lt;&#x2F;code&gt; now being the only thing that owns the outbound cipher state and the write side of the connection. Everything else puts frames on its queue. Nonce order is call order, and length plus body go out in one &lt;code&gt;Write&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The queue holds 16 frames, so it needs a policy for when it&#x27;s full. Deliver frames block (anything not written never gets acked, so it stays in the inbox). Heartbeat echoes get dropped, because the read loop also handles acks and shouldn&#x27;t stall behind a slow connection.&lt;&#x2F;p&gt;
&lt;p&gt;That dropped echo leaks into the client. If the relay can drop echoes under load, the client can&#x27;t use &quot;I got my echo back&quot; to decide the connection is alive. So any inbound frame now counts as liveness, and the client severs the session if nothing arrives in 75 seconds.&lt;&#x2F;p&gt;
&lt;p&gt;This is far from perfect, but at least a way to move forward.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-rust-side-had-a-cousin&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-rust-side-had-a-cousin&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The Rust side had a cousin&lt;&#x2F;h2&gt;
&lt;p&gt;The Rust client sits on my own noise crate, and it had the same class of bug. Two threads sending on one session could encrypt with nonce n and n+1 and write n+1 first. Two threads receiving could each grab part of the same frame. Both had failing tests, for XX and NK. Now &lt;code&gt;send&lt;&#x2F;code&gt; holds a lock from encrypt through write, &lt;code&gt;receive&lt;&#x2F;code&gt; holds one from the first header byte through decrypt.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>2 day debug session</title>
        <published>2026-05-17T00:00:00+00:00</published>
        <updated>2026-05-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/19-docker-bug-debug-session-trueseal/"/>
        <id>https://buenomini.com/posts/19-docker-bug-debug-session-trueseal/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/19-docker-bug-debug-session-trueseal/">&lt;p&gt;I have a side project called trueseal-sync (coming soon). It&#x27;s an E2EE sync primitive built on top of the Noise protocol. The relay is written in Go, the client in Rust, and they speak Noise XX over plain TCP. Boring on purpose.&lt;&#x2F;p&gt;
&lt;p&gt;Everything worked perfectly when I ran the relay natively with &lt;code&gt;go run&lt;&#x2F;code&gt;. Then I dockerized it. And about 90% of the time the handshake failed with &lt;code&gt;Decrypt&lt;&#x2F;code&gt; (AEAD MAC failure on msg1). Same client. Same relay code. Same machine. Just one ran inside a container and the other didn&#x27;t.&lt;&#x2F;p&gt;
&lt;p&gt;This post is the autopsy. Four wrong theories, one pcap, and a punchline I should&#x27;ve seen at hour one.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;theory-1-it-s-a-partial-tcp-read&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#theory-1-it-s-a-partial-tcp-read&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Theory 1: it&#x27;s a partial TCP read&lt;&#x2F;h2&gt;
&lt;p&gt;This was my first instinct and I was very confident about it. Native works, Docker fails → the difference must be in the network shape.&lt;&#x2F;p&gt;
&lt;p&gt;Loopback always coalesces small writes into a single read. Docker NAT can split them. Noise XX msg1 is &lt;code&gt;[len:2][body:96]&lt;&#x2F;code&gt;, written by the relay as two &lt;code&gt;Write&lt;&#x2F;code&gt; calls. On loopback they arrive as one read but through a bridge they might not. If my framing code only read 2 bytes and assumed the body was there, state would diverge and AEAD would fail. This is also exactly the bug I would expect myself to write, I hand-rolled the framing.&lt;&#x2F;p&gt;
&lt;p&gt;I checked my framing and sessions, &lt;code&gt;read_exact&lt;&#x2F;code&gt; everywhere... there since v0.1.0.&lt;&#x2F;p&gt;
&lt;p class=&quot;verdict&quot;&gt;&lt;span class=&quot;tag&quot;&gt;ruled out&lt;&#x2F;span&gt;&lt;span&gt;Theory dead, as well as my hopes for an easy fix.&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;theory-2-it-s-the-mtu&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#theory-2-it-s-the-mtu&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Theory 2: it&#x27;s the MTU&lt;&#x2F;h2&gt;
&lt;p&gt;I have a weird Docker context because of some company security measures, and this wouldn&#x27;t be the first time it makes my hair fall out. &lt;code&gt;ifconfig&lt;&#x2F;code&gt; on the host showed &lt;code&gt;bridge100&lt;&#x2F;code&gt; at MTU 1500. From inside a container, &lt;code&gt;eth0&lt;&#x2F;code&gt; at MTU &lt;strong&gt;1280&lt;&#x2F;strong&gt;. My Docker context creates a bridged network and apparently picks 1280 for the VM&#x27;s eth0.&lt;&#x2F;p&gt;
&lt;p&gt;Smaller MTU + bigger sender + broken PMTUD = larger segments silently black-holed. New networking stacks ship with weird defaults all the time.&lt;&#x2F;p&gt;
&lt;p&gt;So I bumped the MTU, re-ran the test. Same failure rate. It also occurred to me, slightly too late, that Noise handshake messages are under 100 bytes. MTU 1280 vs 1500 can&#x27;t possibly matter for a 98-byte payload (please don&#x27;t ask me how I know this now, but I think it&#x27;s a clear sign of how much I suffer).&lt;&#x2F;p&gt;
&lt;p class=&quot;verdict&quot;&gt;&lt;span class=&quot;tag&quot;&gt;ruled out&lt;&#x2F;span&gt;&lt;span&gt;Theory dead, as well as my hopes for a complicated fix.&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;theory-3-my-read-path-mishandles-real-network-latency&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#theory-3-my-read-path-mishandles-real-network-latency&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Theory 3: my read path mishandles real network latency&lt;&#x2F;h2&gt;
&lt;p&gt;I captured a pcap on &lt;code&gt;bridge100&lt;&#x2F;code&gt; and looked at it. Sure enough, the relay&#x27;s msg1 arrives as two TCP segments, &lt;code&gt;length 2&lt;&#x2F;code&gt; then &lt;code&gt;length 96&lt;&#x2F;code&gt;. The gap between them is anywhere from 1µs (when they coalesce) to ~70µs (when they don&#x27;t). On the failing attempts the gap was always wider.&lt;&#x2F;p&gt;
&lt;p&gt;New theory was that my client calls &lt;code&gt;set_nonblocking(true)&lt;&#x2F;code&gt; on the TcpStream before the handshake, and the trueseal-noise crate has a &lt;code&gt;RetryConn&lt;&#x2F;code&gt; wrapper that loops on &lt;code&gt;WouldBlock&lt;&#x2F;code&gt;. Maybe &lt;code&gt;RetryConn&lt;&#x2F;code&gt; has a subtle bug that only shows up when reads split &lt;em&gt;and&lt;&#x2F;em&gt; the second read returns &lt;code&gt;WouldBlock&lt;&#x2F;code&gt; first. On loopback the data is always already in the kernel buffer, so the retry path never runs. On Docker it does. This was my favourite theory, quite &quot;elegant&quot; if I may. It explained the timing, the intermittency, and the &quot;only on Docker&quot; pattern.&lt;&#x2F;p&gt;
&lt;p&gt;I vibe coded a debug wrapper called &lt;code&gt;SlowStream&lt;&#x2F;code&gt; that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;caps every &lt;code&gt;read()&lt;&#x2F;code&gt; to 1 byte,&lt;&#x2F;li&gt;
&lt;li&gt;sleeps 5ms before each call,&lt;&#x2F;li&gt;
&lt;li&gt;returns &lt;code&gt;WouldBlock&lt;&#x2F;code&gt; on every other call to force the retry loop.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Then I pointed the client at the &lt;em&gt;native&lt;&#x2F;em&gt; relay (loopback, where it normally works 100% of the time).&lt;&#x2F;p&gt;
&lt;p&gt;Handshake succeeded. Every time. Slowly, but successfully.&lt;&#x2F;p&gt;
&lt;p class=&quot;verdict&quot;&gt;&lt;span class=&quot;tag&quot;&gt;ruled out&lt;&#x2F;span&gt;&lt;span&gt;Theory dead, as well as my desire to live.&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;theory-4-the-bridge-is-mangling-bytes&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#theory-4-the-bridge-is-mangling-bytes&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Theory 4: the bridge is mangling bytes&lt;&#x2F;h2&gt;
&lt;p&gt;By this point I was running out of ideas. Let&#x27;s at least &lt;em&gt;look&lt;&#x2F;em&gt; at the bytes.&lt;&#x2F;p&gt;
&lt;p&gt;I added byte-level logging in two places: the client (every &lt;code&gt;read()&lt;&#x2F;code&gt; and &lt;code&gt;write()&lt;&#x2F;code&gt; on the TcpStream, hex-dumped) and the relay (the exact &lt;code&gt;msg0&lt;&#x2F;code&gt; it received and the exact &lt;code&gt;msg1&lt;&#x2F;code&gt; it sent, hex-dumped). Then I ran tcpdump in parallel and grabbed the wire bytes.&lt;&#x2F;p&gt;
&lt;p&gt;Three views of the same handshake:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;client wrote msg0 → e6b22…de71
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;wire stream  msg0 → e6b22…de71
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;relay  read  msg0 → e6b22…de71
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;relay  wrote msg1 → 5890800f…7e9ece
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;wire stream  msg1 → 5890800f…7e9ece
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;client read  msg1 → 5890800f…7e9ece
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Bit-identical across all three. For every failing attempt. The bytes weren&#x27;t mangled &lt;em&gt;at all&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;And yet &lt;code&gt;chacha20poly1305: message authentication failed&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This is the moment in any debugging session where you start to question your career choices. The bytes are right on both sides, the protocol is right, the framing is right. AEAD still fails. Either snow is broken (it&#x27;s not, this code has worked for years and is properly battle tested), or the math itself is wrong somewhere (also not quite possible).&lt;&#x2F;p&gt;
&lt;p&gt;If the bytes are identical but the math disagrees, one side computed those &quot;correct&quot; bytes from the &lt;em&gt;wrong&lt;&#x2F;em&gt; inputs. Both ends are honest about what they sent and received. They aren&#x27;t computing the same things from them. Math was indeed not mathing.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s a CPU bug.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cpu-was-lying&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#cpu-was-lying&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
CPU was lying&lt;&#x2F;h2&gt;
&lt;p&gt;I ran one command I should&#x27;ve run at hour one:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh z-code&quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-shell&quot;&gt; docker run&lt;span class=&quot;z-variable z-parameter z-option z-shell&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-parameter z-shell&quot;&gt; --&lt;&#x2F;span&gt;rm&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter z-option z-shell&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-parameter z-shell&quot;&gt; --&lt;&#x2F;span&gt;net&lt;&#x2F;span&gt; host alpine uname&lt;span class=&quot;z-variable z-parameter z-option z-shell&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-parameter z-shell&quot;&gt; -&lt;&#x2F;span&gt;m&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;aarch64&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK, the VM is arm64. That&#x27;s expected, I&#x27;m on Apple Silicon.&lt;&#x2F;p&gt;
&lt;p&gt;Then I dared to check the Dockerfile:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;dockerfile&quot; class=&quot;language-dockerfile z-code&quot;&gt;&lt;code class=&quot;language-dockerfile&quot; data-lang=&quot;dockerfile&quot;&gt;&lt;span class=&quot;z-source z-dockerfile&quot;&gt;RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-dockerfile&quot;&gt;    go build -ldflags=&amp;quot;-s -w&amp;quot; -trimpath \
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-dockerfile&quot;&gt;    -o &#x2F;trueseal-relay .&#x2F;cmd&#x2F;trueseal-relay
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;GOARCH=amd64&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I was building an x86-64 binary and running it on an arm64 VM. The Linux kernel sees a foreign ELF, hands it off to qemu-user, and qemu translates every instruction at runtime.&lt;&#x2F;p&gt;
&lt;figure class=&quot;inline has-port&quot;&gt;
  &lt;svg class=&quot;fig fig-land&quot; viewBox=&quot;0 0 690 290&quot; role=&quot;img&quot; aria-label=&quot;What was running: a native arm64 client talks through the Docker bridge to an arm64 Linux VM. Inside it, qemu-user translates the relay, which was built as an amd64 binary.&quot;&gt;&lt;defs&gt;&lt;marker id=&quot;ah-docker-land&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path class=&quot;arrow-dim&quot; d=&quot;M0 0 L10 5 L0 10 z&quot;&#x2F;&gt;&lt;&#x2F;marker&gt;&lt;&#x2F;defs&gt;&lt;rect class=&quot;box&quot; x=&quot;2&quot; y=&quot;112&quot; width=&quot;150&quot; height=&quot;62&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;18&quot; y=&quot;139&quot;&gt;Client&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;18&quot; y=&quot;160&quot;&gt;Rust, native arm64&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M152 143 L190 143&quot; marker-end=&quot;url(#ah-docker-land)&quot;&#x2F;&gt;&lt;rect class=&quot;box&quot; x=&quot;192&quot; y=&quot;118&quot; width=&quot;116&quot; height=&quot;50&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;208&quot; y=&quot;140&quot;&gt;Bridge&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;208&quot; y=&quot;158&quot;&gt;bridge100&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M308 143 L384 143&quot; marker-end=&quot;url(#ah-docker-land)&quot;&#x2F;&gt;&lt;rect class=&quot;box outer&quot; x=&quot;346&quot; y=&quot;10&quot; width=&quot;340&quot; height=&quot;270&quot; rx=&quot;12&quot;&#x2F;&gt;&lt;text x=&quot;364&quot; y=&quot;38&quot;&gt;Linux VM&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;364&quot; y=&quot;58&quot;&gt;aarch64&lt;&#x2F;text&gt;&lt;rect class=&quot;box relay&quot; x=&quot;368&quot; y=&quot;76&quot; width=&quot;296&quot; height=&quot;182&quot; rx=&quot;10&quot;&#x2F;&gt;&lt;text x=&quot;386&quot; y=&quot;102&quot;&gt;qemu-user&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;386&quot; y=&quot;122&quot;&gt;translating every x86-64 instruction&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;388&quot; y=&quot;140&quot; width=&quot;256&quot; height=&quot;96&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;406&quot; y=&quot;170&quot;&gt;Relay&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;406&quot; y=&quot;192&quot;&gt;Go, built with&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;406&quot; y=&quot;212&quot;&gt;GOARCH=amd64&lt;&#x2F;text&gt;&lt;&#x2F;svg&gt;
&lt;svg class=&quot;fig fig-port&quot; viewBox=&quot;0 0 360 520&quot; role=&quot;img&quot; aria-label=&quot;What was running: a native arm64 client talks through the Docker bridge to an arm64 Linux VM. Inside it, qemu-user translates the relay, which was built as an amd64 binary.&quot;&gt;&lt;defs&gt;&lt;marker id=&quot;ah-docker-port&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path class=&quot;arrow-dim&quot; d=&quot;M0 0 L10 5 L0 10 z&quot;&#x2F;&gt;&lt;&#x2F;marker&gt;&lt;&#x2F;defs&gt;&lt;rect class=&quot;box&quot; x=&quot;90&quot; y=&quot;2&quot; width=&quot;180&quot; height=&quot;58&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;106&quot; y=&quot;27&quot;&gt;Client&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;106&quot; y=&quot;47&quot;&gt;Rust, native arm64&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M180 60 L180 86&quot; marker-end=&quot;url(#ah-docker-port)&quot;&#x2F;&gt;&lt;rect class=&quot;box&quot; x=&quot;105&quot; y=&quot;88&quot; width=&quot;150&quot; height=&quot;48&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;121&quot; y=&quot;109&quot;&gt;Bridge&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;121&quot; y=&quot;127&quot;&gt;bridge100&lt;&#x2F;text&gt;&lt;path class=&quot;edge dash&quot; d=&quot;M180 136 L180 236&quot; marker-end=&quot;url(#ah-docker-port)&quot;&#x2F;&gt;&lt;rect class=&quot;box outer&quot; x=&quot;4&quot; y=&quot;172&quot; width=&quot;352&quot; height=&quot;344&quot; rx=&quot;12&quot;&#x2F;&gt;&lt;text x=&quot;22&quot; y=&quot;198&quot;&gt;Linux VM&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;22&quot; y=&quot;217&quot;&gt;aarch64&lt;&#x2F;text&gt;&lt;rect class=&quot;box relay&quot; x=&quot;22&quot; y=&quot;238&quot; width=&quot;316&quot; height=&quot;260&quot; rx=&quot;10&quot;&#x2F;&gt;&lt;text x=&quot;40&quot; y=&quot;264&quot;&gt;qemu-user&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;40&quot; y=&quot;284&quot;&gt;translating every x86-64 instruction&lt;&#x2F;text&gt;&lt;rect class=&quot;box&quot; x=&quot;40&quot; y=&quot;304&quot; width=&quot;280&quot; height=&quot;172&quot; rx=&quot;8&quot;&#x2F;&gt;&lt;text x=&quot;58&quot; y=&quot;336&quot;&gt;Relay&lt;&#x2F;text&gt;&lt;text class=&quot;sub&quot; x=&quot;58&quot; y=&quot;360&quot;&gt;Go, built with&lt;&#x2F;text&gt;&lt;text class=&quot;sub key&quot; x=&quot;58&quot; y=&quot;382&quot;&gt;GOARCH=amd64&lt;&#x2F;text&gt;&lt;&#x2F;svg&gt;

  &lt;figcaption&gt;Figure 1. What was running on my laptop. The relay was an amd64 binary, so qemu translated every instruction it ran.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Most of the time this works, web servers run, databases run. Business as usual.&lt;&#x2F;p&gt;
&lt;p&gt;But flynn&#x2F;noise does Curve25519, ChaCha20-Poly1305 and BLAKE2s, which are full of 64-bit multiplies with carry chains, bit rotations and constant-time arithmetic (and god knows what else). qemu&#x27;s x86 emulation on arm64 has a long history of being subtly wrong on a fraction of the operations crypto code does (I had a vague memory of something like this happening to me when I tried to build a simple OS MMU). Not &lt;em&gt;always&lt;&#x2F;em&gt; wrong, but wrong &lt;em&gt;sometimes&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;So the relay was computing X25519 shared secrets that were &lt;em&gt;occasionally&lt;&#x2F;em&gt; off by a few bits, and both ends wrote bytes that exactly reflected what they computed.&lt;&#x2F;p&gt;
&lt;p&gt;The bytes were correct, but math ain&#x27;t mathing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-was-that-line-even-there&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#why-was-that-line-even-there&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Why was that line even there&lt;&#x2F;h2&gt;
&lt;p&gt;This is the part where I want to be nice to past me. And at the same time murder him.&lt;&#x2F;p&gt;
&lt;p&gt;I vibe coded the Dockerfile for a Hostinger VPS on the cheapest tier (hehe), and the AI pinned it to amd64, which was OK for what I told it to do. I wanted to be able to &lt;code&gt;docker push&lt;&#x2F;code&gt; from my laptop and &lt;em&gt;know&lt;&#x2F;em&gt; the binary on the server would match the server&#x27;s CPU. That was all OK until I &lt;em&gt;dared&lt;&#x2F;em&gt; to test what the AI did before pushing the changes, which started 2 days of headbanging against the keyboard.&lt;&#x2F;p&gt;
&lt;p&gt;The AI optimized for one deployment target and forgot that the same Dockerfile also builds the images I run &lt;em&gt;locally&lt;&#x2F;em&gt;, on the opposite architecture, every time I run &lt;code&gt;docker-compose up&lt;&#x2F;code&gt;. The pin guaranteed correctness on the VPS at the cost of correctness on my dev machine. But I can&#x27;t blame the pig, it just did what I asked it to.&lt;&#x2F;p&gt;
&lt;p&gt;In a way it did better than I would&#x27;ve done myself, I wouldn&#x27;t have pinned the architecture at all, it didn&#x27;t even cross my mind. I&#x27;m sure there&#x27;s an important takeaway here... but I&#x27;m too tired to think about it today, this post is running on fumes already. Maybe something like:&lt;&#x2F;p&gt;
&lt;p&gt;The relay is zero-trust on the network. Turns out you also can&#x27;t trust your emulated CPU, or myself for that matter.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Anchors: first test</title>
        <published>2026-05-05T00:00:00+00:00</published>
        <updated>2026-05-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/18-anchors-first-test/"/>
        <id>https://buenomini.com/posts/18-anchors-first-test/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/18-anchors-first-test/">&lt;p&gt;Quick update on &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;14-anchors-early-results&#x2F;&quot;&gt;anchors&lt;&#x2F;a&gt;. I&#x27;ve been cooking.&lt;&#x2F;p&gt;
&lt;p&gt;The original idea was static analysis + LLM enhancement, outputting YAML files the LLM could read before answering questions about a codebase. That worked, but it was clunky. The YAML files were big, slow to generate, and didn&#x27;t scale well.&lt;&#x2F;p&gt;
&lt;p&gt;So I pivoted. anchors now builds a SQLite database with the full call graph already in it (methods, call relationships, file locations, etc.) and the LLM doesn&#x27;t read files anymore, it queries the graph directly. This is preliminary but the results surprised me enough that I wanted to write about it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-setup&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-setup&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The setup&lt;&#x2F;h2&gt;
&lt;p&gt;I pointed anchors at a production TypeScript codebase I worked on, around 600 files and 180k lines of code. I had a known bug as ground truth (a batching issue where defaulting to chunks of 1 was making performance worse), and the prompt I used was intentionally vague:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;We&#x27;re getting reports that [feature x] loading is slow for power users with lots of [y].&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Just the kind of thing someone might actually say in a bug report.&lt;&#x2F;p&gt;
&lt;p&gt;I expected anchors to be faster and cheaper. The call graph should let the LLM skip the grep-and-wander phase and go straight to the relevant code. That&#x27;s not what happened. The results were encouraging and weird:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;&#x2F;th&gt;&lt;th&gt;Without Anchors&lt;&#x2F;th&gt;&lt;th&gt;With Anchors&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Found the correct bug?&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;What it found&lt;&#x2F;td&gt;&lt;td&gt;Different bug (missing ORDER BY)&lt;&#x2F;td&gt;&lt;td&gt;The batching bug&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Cost&lt;&#x2F;td&gt;&lt;td&gt;$0.09&lt;&#x2F;td&gt;&lt;td&gt;$0.36&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Wall time&lt;&#x2F;td&gt;&lt;td&gt;1m 19s&lt;&#x2F;td&gt;&lt;td&gt;3m 10s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Files read&lt;&#x2F;td&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Approach&lt;&#x2F;td&gt;&lt;td&gt;grep → read files&lt;&#x2F;td&gt;&lt;td&gt;query graph → trace → read files&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Without anchors, the LLM grep&#x27;d for the feature, found a different (valid!) performance issue, a missing &lt;code&gt;ORDER BY&lt;&#x2F;code&gt; clause in a SQL query, and called it a day.&lt;&#x2F;p&gt;
&lt;p&gt;With anchors it queried the call graph, traced the path step by step, and found the actual batching bug. It even ran Node to confirm that lodash &lt;code&gt;chunk()&lt;&#x2F;code&gt; defaults to chunks of size 1. Slower and more expensive but at least the correct bug.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-surprised-me&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-surprised-me&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What surprised me&lt;&#x2F;h2&gt;
&lt;p&gt;I might be wrong about speed and cost. Anchors was 4x more expensive and took twice as long, and I&#x27;m not sure if that has to do with MCPs and the context window they use. But it found &lt;em&gt;the&lt;&#x2F;em&gt; bug instead of just &lt;em&gt;a&lt;&#x2F;em&gt; bug, which is a different kind of accuracy. The LLM without anchors did what LLMs do, find something plausible quickly and stop. The one with anchors traced the actual call chain from the entry point to the storage layer.&lt;&#x2F;p&gt;
&lt;p&gt;One test proves nothing tho. The prompt was vague enough that &quot;finding a different bug&quot; isn&#x27;t really wrong...&lt;&#x2F;p&gt;
&lt;p&gt;Next is probably more tests, and see how it evolves. I might build some automated eval process and run it on some mid-sized open source project.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>A or B, not A&#x2F;B</title>
        <published>2026-05-02T00:00:00+00:00</published>
        <updated>2026-05-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/17-a-or-b/"/>
        <id>https://buenomini.com/posts/17-a-or-b/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/17-a-or-b/">&lt;p&gt;I&#x27;m currently looking for a new &quot;home&quot; for my knowledge base, and then I came across &lt;a href=&quot;https:&#x2F;&#x2F;anytype.io&quot;&gt;anytype&lt;&#x2F;a&gt;. Which is what triggered this thought.&lt;&#x2F;p&gt;
&lt;p&gt;I like opinionated software. It attracts the right people and repels the wrong ones, so you (as an engineer) can focus on the group of people that actually matters. When a product has a clear perspective you don&#x27;t have to wonder what it&#x27;s trying to be, you just need to figure out if your opinion matches.&lt;&#x2F;p&gt;
&lt;p&gt;A&#x2F;B testing is the opposite of that. And it rubs me the wrong way.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;it-is-great-in-theory&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#it-is-great-in-theory&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
It is great, in theory...&lt;&#x2F;h2&gt;
&lt;p&gt;A&#x2F;B testing sounds rigorous. Data-driven! Scientific! You&#x27;re &lt;em&gt;testing hypotheses&lt;&#x2F;em&gt; instead of guessing. Which sometimes is true.&lt;&#x2F;p&gt;
&lt;p&gt;But something about it has always bothered me, or at least about how I&#x27;ve seen it implemented and abused. It changes the question from &quot;what &lt;em&gt;should&lt;&#x2F;em&gt; we build?&quot; to &quot;let&#x27;s see what performs better&quot;, and those are different questions. The first one takes deep thought, conviction and a real sense of what&#x27;s right for your users. The second one just takes two variants and enough traffic.&lt;&#x2F;p&gt;
&lt;p&gt;When A&#x2F;B testing becomes the default the incentives change. Instead of thinking a feature through before shipping it, you ship two half-baked versions and see which one &quot;wins&quot;. Thinking is slow and A&#x2F;B tests are fast. And stakeholders ain&#x27;t got time, you know, time is money...&lt;&#x2F;p&gt;
&lt;p&gt;So you end up with a product shaped by whatever happened to perform better in a two-week experiment, instead of a coherent idea of what it should be.&lt;&#x2F;p&gt;
&lt;p&gt;(And btw, some portion of your users got the shitty experience along the way. Where&#x27;s the care in that?)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-the-data-doesn-t-tell-you&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-the-data-doesn-t-tell-you&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What the data doesn&#x27;t tell you&lt;&#x2F;h2&gt;
&lt;p&gt;A&#x2F;B tests optimize for what you can measure: clicks, conversions, time on page. They can&#x27;t measure trust, craft, or whether a feature &lt;em&gt;should&lt;&#x2F;em&gt; exist at all. So you optimize for the numbers, and the numbers tell you what people clicked. They don&#x27;t tell you what people respected.&lt;&#x2F;p&gt;
&lt;p&gt;But look... I&#x27;m not saying never experiment. Trying stuff is good, learning from users is good. I mean &quot;we believe X is the right approach, let&#x27;s validate it&quot; is a different thing from &quot;we have two ideas, let&#x27;s see which gets more clicks&quot;. The first is thoughtful, the second is outsourcing your decisions.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Pick A. Or pick B. But pick it because you thought about it, instead of hoping the data will think for you. Opinionated software comes from opinionated people making actual decisions, and that&#x27;s the kind I want to use and build.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The spectrum of error handling</title>
        <published>2026-04-11T00:00:00+00:00</published>
        <updated>2026-04-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/16-assertions/"/>
        <id>https://buenomini.com/posts/16-assertions/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/16-assertions/">&lt;p&gt;I&#x27;ve been reading about how TigerBeetle approaches correctness, and one thing that stuck with me is their aggressive use of assertions.&lt;&#x2F;p&gt;
&lt;p&gt;Most codebases I&#x27;ve worked on treat assertions as a debug or testing tool. You rarely sprinkle them in during development, and it&#x27;s even less common to leave them in prod. TigerBeetle does the opposite, assertions stay in prod and they&#x27;re a core part of the correctness strategy. That got me thinking about error handling more broadly, because there&#x27;s a spectrum here that most code blurs together, and I think that&#x27;s a mistake.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;three-categories-of-something-went-wrong&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#three-categories-of-something-went-wrong&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Three categories of &quot;something went wrong&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;The first one is expected failures, the recoverable kind. Network is down, file doesn&#x27;t exist, user gave you garbage input, whatever, right? These aren&#x27;t bugs, they&#x27;re normal operation and the system should handle them gracefully.&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;function read_config(path):
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    contents = read_file(path)    &#x2F;&#x2F; might fail - file doesn&amp;#39;t exist
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    return parse(contents)         &#x2F;&#x2F; might fail - malformed
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The caller decides what to do.&lt;&#x2F;p&gt;
&lt;p&gt;Then there are bugs, the stuff that should never happen. An index out of bounds or an NPE where one should never exist, a state your code assumed was impossible. You don&#x27;t handle these gracefully. Something is already wrong with your program, and if you&#x27;re here your assumptions about reality are broken.&lt;&#x2F;p&gt;
&lt;p&gt;And the third one is invariant violations, where corruption is worse than crashing. This is TigerBeetle&#x27;s territory: things that should &lt;em&gt;always&lt;&#x2F;em&gt; be true about your system. In their domain a bank balance should never be negative (unless you allow overdrafts), that kind of stuff. If one of these breaks something is deeply wrong. Maybe a cosmic ray flipped a bit (I heard this happened during an election, more &lt;a href=&quot;https:&#x2F;&#x2F;www.vice.com&#x2F;en&#x2F;article&#x2F;space-weather-cosmic-rays-voting-aaas&#x2F;&quot;&gt;here&lt;&#x2F;a&gt;). Maybe (probably) there&#x27;s a bug you haven&#x27;t found. Maybe memory got corrupted. Whatever the case, if you keep going you might spread the corruption through the rest of the system.&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;function transfer(from, to, amount):
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    from.balance -= amount
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    to.balance += amount
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    &#x2F;&#x2F; Invariant: total money in system should be constant
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    assert(from.balance + to.balance == TOTAL_EXPECTED)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;TigerBeetle&#x27;s answer is to crash and restart from a known-good state. A crash is recoverable but corrupted data spreading silently is not.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-most-code-blurs-these-together&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#why-most-code-blurs-these-together&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Why most code blurs these together&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ve seen (and written) code that treats all three the same way:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;function process(data):
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    if data.items is empty:
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;        return Error(&amp;quot;empty data&amp;quot;)  &#x2F;&#x2F; Is this a bug or expected input?
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;    &#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The caller can&#x27;t tell if &lt;code&gt;EmptyData&lt;&#x2F;code&gt; means &quot;user gave bad input, show them a message&quot; or &quot;something is fundamentally broken, we should crash&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;Defensive programming makes this worse. &quot;Handle every error gracefully&quot; sounds safe, but it can mean silently continuing when you shouldn&#x27;t, and you end up with systems that limp along in a corrupted state instead of failing loudly at the first sign of trouble.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-tigerbeetle-stance&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-tigerbeetle-stance&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The TigerBeetle stance&lt;&#x2F;h2&gt;
&lt;p&gt;TigerBeetle is building a financial database, so correctness matters more than uptime. Their position, based on their docs and my interpretation of them, is:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;If an invariant is important enough to check, it&#x27;s important enough to crash for&lt;&#x2F;li&gt;
&lt;li&gt;Assertions stay in production&lt;&#x2F;li&gt;
&lt;li&gt;A crash + restart from a clean state beats silent corruption&lt;&#x2F;li&gt;
&lt;li&gt;&quot;Impossible&quot; states get asserted instead of handled &amp;lt;- big one here&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;That&#x27;s extreme for most applications. But the principle underneath is sound... be clear about which category each failure falls into, and handle each one accordingly.&lt;&#x2F;p&gt;
&lt;p&gt;I haven&#x27;t tried this in my own code yet, but I want to on my next project. The shift is to stop treating all errors the same, so while I&#x27;m hitting the keyboard with my head (how I mostly produce code) I&#x27;ll ask myself:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Is this expected? Let the caller decide.&lt;&#x2F;li&gt;
&lt;li&gt;Is this a bug, a state I didn&#x27;t expect? Panic. Fix the bug.&lt;&#x2F;li&gt;
&lt;li&gt;Is this a state that should never occur? Assert it in production, crash if it happens.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The third one is what I&#x27;ve mostly been ignoring. I always try to &quot;handle&quot; impossible states, which in practice means hiding them. If a state or invariant breaks I want to know immediately, and not three hours after a deploy during an incident.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;We&#x27;ll see how it goes. Maybe I&#x27;ll write a follow-up once I&#x27;ve actually tried being more aggressive with assertions. For now it&#x27;s just an idea that sounds great in theory but is maybe overkill for projects that aren&#x27;t high stakes, and error handling is apparently something I have a lot to learn about.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Why I never finish side projects (and why that&#x27;s fine)</title>
        <published>2026-04-08T00:00:00+00:00</published>
        <updated>2026-04-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/15-side-quests/"/>
        <id>https://buenomini.com/posts/15-side-quests/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/15-side-quests/">&lt;p&gt;I started rkv to learn Rust and Zig. Four modules in, I got distracted by &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;14-anchors-early-results&#x2F;&quot;&gt;anchors&lt;&#x2F;a&gt;, a completely unrelated project about static analysis and LLMs, so now I have two unfinished projects instead of one.&lt;&#x2F;p&gt;
&lt;p&gt;Before that I built &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;01-hush&#x2F;&quot;&gt;hush&lt;&#x2F;a&gt; to learn Go and networking. It works, but I stopped adding features the moment I understood PAKE and TCP framing.&lt;&#x2F;p&gt;
&lt;p&gt;This keeps happening. I start something, get deep into it, then wander off before it&#x27;s &quot;done&quot;. By conventional standards my GitHub is a graveyard of abandoned projects. I don&#x27;t think that&#x27;s a problem.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-conventional-wisdom-is-wrong&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-conventional-wisdom-is-wrong&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The conventional wisdom is wrong&lt;&#x2F;h2&gt;
&lt;p&gt;&quot;Finish what you start&quot; sounds reasonable, but it doesn&#x27;t always apply. The advice assumes your goal is to ship a product and have something to show for it (a portfolio piece, a tool other people use, a line on your resume). That was never my goal. I pick a project to learn something I don&#x27;t understand, and the project is just the vehicle. Once I&#x27;ve learned what I came for it has done its job, and continuing past that point is mostly inertia.&lt;&#x2F;p&gt;
&lt;p&gt;When I lose interest in a project it&#x27;s usually because it got easy. With rkv the first four modules were intense: memory management in Zig, fighting the borrow checker in Rust, figuring out how event loops actually work. I was confused about something every day and every day I learned something I didn&#x27;t know before.&lt;&#x2F;p&gt;
&lt;p&gt;Then I got to module 5 and I knew what to do. The architecture was clear and the remaining work was... implementation. Important if you&#x27;re shipping a product, boring if you&#x27;re trying to learn. The learning curve had flattened and the easy part doesn&#x27;t teach you anything.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;side-quests-come-from-curiosity&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#side-quests-come-from-curiosity&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Side quests come from curiosity&lt;&#x2F;h2&gt;
&lt;p&gt;Anchors didn&#x27;t interrupt rkv at random, it came out of rkv. Building an event loop got me thinking about code as graphs of function calls, and that was fresh in my head when I ran into the code archaeology problem at work. The connection was clear to me... what if static analysis did the graph traversal and the LLM did what it&#x27;s good at?&lt;&#x2F;p&gt;
&lt;p&gt;That question was more interesting than finishing modules 5-7 of rkv. So I followed it.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s kind of how side quests work for me. Project A teaches you something that lets you ask a question you couldn&#x27;t have asked before, and project B comes out of that question. Compounding curiosity, if you want a name for it!&lt;&#x2F;p&gt;
&lt;p&gt;Not all project-hopping is equal tho. Sometimes you&#x27;re following curiosity: you learned something that opened a new door, the new thing is more interesting than the old one, and you&#x27;re moving toward something. Other times you hit a wall, something you don&#x27;t understand, and instead of pushing through you start something new, which is moving away from something. The first one is how learning works. The second one is procrastination with extra steps.&lt;&#x2F;p&gt;
&lt;p&gt;How do I tell them apart? Honestly it&#x27;s not always clear, but I have a rough rule. If you&#x27;re leaving because the remaining work is &lt;em&gt;too easy&lt;&#x2F;em&gt;, that&#x27;s curiosity. If you&#x27;re leaving because it&#x27;s &lt;em&gt;too hard&lt;&#x2F;em&gt;, that&#x27;s avoidance.&lt;&#x2F;p&gt;
&lt;p&gt;With rkv I could tell you exactly how to implement the remaining modules. There&#x27;s no mystery left and that&#x27;s why I stopped. The encryption module I had planned is a different story. I didn&#x27;t understand it well enough to implement it, so if I had stopped &lt;em&gt;there&lt;&#x2F;em&gt; it would have been quitting cause it was hard.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unfinished-doesn-t-mean-failed&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#unfinished-doesn-t-mean-failed&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Unfinished doesn&#x27;t mean failed&lt;&#x2F;h2&gt;
&lt;p&gt;Each of these projects taught me something. None of them is &quot;finished&quot;, and all of them made me a better engineer. The understanding is what I was after, and once I have it the code is just... code. I can write it anytime, and I probably don&#x27;t need to.&lt;&#x2F;p&gt;
&lt;p&gt;And even if I forget how something specifically works, exposing yourself to new paradigms is valuable in itself. Gives you diversity of thought.&lt;&#x2F;p&gt;
&lt;p&gt;In my &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;05-learning-framework&#x2F;&quot;&gt;learning framework post&lt;&#x2F;a&gt; I wrote about structure: knowledge bases, project logs, writing things down. That still matters. The logs help you consolidate what you learned, they don&#x27;t oblige you to keep going after the learning stops. If anything they make it &lt;em&gt;easier&lt;&#x2F;em&gt; to move on, because once I&#x27;ve written down what I learned I don&#x27;t need to keep the project alive to remember it.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;So will I finish rkv? Will anchors become a real tool or stay a proof of concept? I don&#x27;t know, and honestly I don&#x27;t care that much. I understand things today that I didn&#x27;t six months ago, and the unfinished projects are how I got here.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Side quest: anchors (early results)</title>
        <published>2026-03-01T00:00:00+00:00</published>
        <updated>2026-03-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/14-anchors-early-results/"/>
        <id>https://buenomini.com/posts/14-anchors-early-results/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/14-anchors-early-results/">&lt;p&gt;In the &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;13-anchors-side-quest&#x2F;&quot;&gt;last post&lt;&#x2F;a&gt; I talked about the code archaeology problem, AI tools getting lost in big codebases. This is what I&#x27;ve been building since.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-workflow&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-workflow&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The workflow&lt;&#x2F;h2&gt;
&lt;p&gt;You drop an anchor comment on an entry point:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java z-code&quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;span class=&quot;z-source z-java&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-java&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-java&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; @anchor:get-recommendations
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-java&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-java&quot;&gt;public&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-class z-java&quot;&gt;CompletableFuture&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-generic z-java&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-generic z-begin z-java&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-class z-java&quot;&gt;Page&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-generic z-java&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-generic z-begin z-java&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-class z-java&quot;&gt;Item&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-generic z-end z-java&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-generic z-end z-java&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-function-call z-java&quot;&gt;&lt;span class=&quot;z-variable z-function z-java&quot;&gt;getRecommendationsAsync&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-java&quot;&gt;(&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-java&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-java&quot;&gt;    &lt;span class=&quot;z-storage z-type z-primitive z-java&quot;&gt;long&lt;&#x2F;span&gt; userId&lt;span class=&quot;z-punctuation z-separator z-comma z-java&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-class z-java&quot;&gt;RecommendationType&lt;&#x2F;span&gt; type&lt;span class=&quot;z-punctuation z-separator z-comma z-java&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-primitive z-java&quot;&gt;int&lt;&#x2F;span&gt; pageSize&lt;span class=&quot;z-punctuation z-section z-parens z-end z-java&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-block z-java&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-java&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-java&quot;&gt;&lt;span class=&quot;z-meta z-block z-java&quot;&gt;    &lt;span class=&quot;z-keyword z-control z-flow z-return z-java&quot;&gt;return&lt;&#x2F;span&gt; recommendationEngine&lt;span class=&quot;z-punctuation z-accessor z-dot z-java&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-java&quot;&gt;&lt;span class=&quot;z-variable z-function z-java&quot;&gt;firstPage&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-java&quot;&gt;(&lt;&#x2F;span&gt;userId&lt;span class=&quot;z-punctuation z-separator z-comma z-java&quot;&gt;,&lt;&#x2F;span&gt; type&lt;span class=&quot;z-punctuation z-separator z-comma z-java&quot;&gt;,&lt;&#x2F;span&gt; pageSize&lt;span class=&quot;z-punctuation z-section z-parens z-end z-java&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-java&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-java&quot;&gt;&lt;span class=&quot;z-meta z-block z-java&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-java&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then you run the analyzer, which uses tree-sitter to parse the code, builds a call graph from that entry point and spits out an &lt;code&gt;.anchor.yaml&lt;&#x2F;code&gt; file. Then you run enhance. The LLM reads the method signatures and adds business context, parameter descriptions, debug keywords, stuff that helps future prompts understand &lt;em&gt;what&lt;&#x2F;em&gt; the code does and not only &lt;em&gt;how&lt;&#x2F;em&gt; it&#x27;s structured.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-an-anchor-file-looks-like-for-now&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-an-anchor-file-looks-like-for-now&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What an anchor file looks like for now&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml z-code&quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;&lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;anchor_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;get-recommendations&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;&lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;methods&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;  &lt;span class=&quot;z-punctuation z-definition z-block z-sequence z-item z-yaml&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;ApiService.getRecommendationsAsync&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;file&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;src&#x2F;...&#x2F;ApiService.java&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;line_range&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-flow-sequence z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-begin z-yaml&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-yaml&quot;&gt;279&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-sequence z-yaml&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-yaml&quot;&gt;290&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-end z-yaml&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;method_signature&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;public CompletableFuture&amp;lt;Page&amp;lt;Item&amp;gt;&amp;gt; getRecommendationsAsync(...)&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-comment z-line z-number-sign z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-line z-number-sign z-yaml&quot;&gt;#&lt;&#x2F;span&gt; LLM-enhanced fields
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;business_context&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-flow z-block-scalar z-literal z-yaml&quot;&gt;|&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;&lt;span class=&quot;z-string z-unquoted z-block z-yaml&quot;&gt;      Main API endpoint for recommendations feature. Returns paginated items
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;&lt;span class=&quot;z-string z-unquoted z-block z-yaml&quot;&gt;      based on user&amp;#39;s history and subscription preferences.
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;&lt;span class=&quot;z-string z-unquoted z-block z-yaml&quot;&gt;&lt;&#x2F;span&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;parameters&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;      &lt;span class=&quot;z-punctuation z-definition z-block z-sequence z-item z-yaml&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;userId&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;        &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;description&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;User identifier&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;      &lt;span class=&quot;z-punctuation z-definition z-block z-sequence z-item z-yaml&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;type&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;        &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;description&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;Type of recommendation view (TRENDING, RECENT, etc.)&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;debug_keywords&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-flow-sequence z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-begin z-yaml&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;recommendations&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-sequence z-yaml&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;ranking&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-sequence z-yaml&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;personalization&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-end z-yaml&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;calls_methods&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-flow-sequence z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-begin z-yaml&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;RecommendationEngine.firstPage&lt;span class=&quot;z-punctuation z-definition z-string z-end z-yaml&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-end z-yaml&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-yaml&quot;&gt;    &lt;span class=&quot;z-string z-unquoted z-plain z-out z-yaml&quot;&gt;&lt;span class=&quot;z-entity z-name z-tag z-yaml&quot;&gt;called_by_methods&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-key-value z-mapping z-yaml&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-flow-sequence z-yaml&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-begin z-yaml&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-definition z-sequence z-end z-yaml&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;business_context&lt;&#x2F;code&gt; says what the method does in human terms, &lt;code&gt;debug_keywords&lt;&#x2F;code&gt; help you find it when you&#x27;re searching for a problem, and &lt;code&gt;calls_methods&lt;&#x2F;code&gt; gives you the full graph without the LLM having to crawl.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-poc&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-poc&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The PoC&lt;&#x2F;h2&gt;
&lt;p&gt;I tested this with a real bug I had already fixed. Old items from years ago were showing at the top of a feed, above recent ones. A sorting issue somewhere in the codebase.&lt;&#x2F;p&gt;
&lt;p&gt;When I fixed it myself it was maybe like 10 minutes of context switching and another 10 creating a test to replicate the behaviour and debug. I don&#x27;t really remember...&lt;&#x2F;p&gt;
&lt;p&gt;Without anchors, Claude CLI crawled through the codebase for 8 minutes. Reading files, backtracking, reading more files. It eventually found the right spot (somewhat impressive).&lt;&#x2F;p&gt;
&lt;p&gt;With anchors I gave the same prompt to a basic chat model with just the anchor file. 15 seconds. It pointed to the exact line in &lt;code&gt;SortingUtils.java:x-y&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Same answer, 32x faster, and the anchor version didn&#x27;t need an agentic model burning tokens on file exploration. I think it works because the anchor file front-loads the hard work. The LLM doesn&#x27;t have to figure out where the sorting logic is, the call graph already traced it, and it doesn&#x27;t have to guess what &lt;code&gt;userId&lt;&#x2F;code&gt; means because the enhanced params explain it. It just reads structured context and reasons about it, which is the thing it&#x27;s good at.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-s-next&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What&#x27;s next&lt;&#x2F;h2&gt;
&lt;p&gt;Still early, not sure this proves anything. It was an overly optimized duct-taped test, and I need to build something a bit more solid to be able to test it systematically. Like using fingerprints (hashes of method bodies) to detect when code changes, so I don&#x27;t re-enhance unchanged methods. Cause right now it takes forever to generate these files...&lt;&#x2F;p&gt;
&lt;p&gt;Not sure how far this goes yet, but the early results are promising enough that I&#x27;m going to keep poking at it. What surprised me is that I thought the main reason to do this was to reduce the hallucinations, but honestly now I see it being very useful for speed&#x2F;token reduction. Even more for large codebases with large teams...&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Side quest: useful AI in complex systems?</title>
        <published>2026-02-27T00:00:00+00:00</published>
        <updated>2026-02-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/13-anchors-side-quest/"/>
        <id>https://buenomini.com/posts/13-anchors-side-quest/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/13-anchors-side-quest/">&lt;p&gt;I work on a medium-sized Java codebase. Multiple libraries, multiple modules, fuzzy service boundaries, and everything talks to everything through different methods (gRPC, Kafka, etc). The classic spaghetti western.&lt;&#x2F;p&gt;
&lt;p&gt;Like everyone else, I&#x27;ve been using AI tools to try to make my mundane tasks easier, and they&#x27;re great for small, contained questions. But ask something that needs the actual flow of the system (i.e. &quot;why is this endpoint slow?&quot; or &quot;where does this field get populated?&quot;) and it falls apart. The AI starts reading files, lots of files, gets overwhelmed, and either hallucinates an answer or gives you something so generic it&#x27;s useless. Many times it&#x27;s just plain wrong.&lt;&#x2F;p&gt;
&lt;p&gt;I thought maybe the problem was context, so I wrote better READMEs and documented the architecture and even made onboarding guides specifically for the AI to read. It helped a little. The hallucinations didn&#x27;t stop though, and the &quot;context rot&quot; didn&#x27;t improve that much, because these tools were still parsing raw code and figuring out what matters on the fly. Every single time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-llms-shine-and-where-they-don-t&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#where-llms-shine-and-where-they-don-t&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Where LLMs shine (and where they don&#x27;t)&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m not an eminence here, but my two cents.&lt;&#x2F;p&gt;
&lt;p&gt;LLMs are really good at semantic understanding. Give them a method signature and they can tell you what it probably does, give them structured context and they compress it nicely. Pattern recognition, summarization, explaining business logic... all great.&lt;&#x2F;p&gt;
&lt;p&gt;Where they&#x27;re terrible is exhaustive traversal. Multi-hop searches through a codebase (maybe less so if you have it vectorized, but still), finding every caller of a function, building a complete picture of how data flows through the system. They miss things, they get lost and then they guess. And you know what&#x27;s good at exhaustive traversal? Static analysis. Compilers and tools like them have been doing it for decades.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-idea-i-m-exploring&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-idea-i-m-exploring&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The idea I&#x27;m exploring&lt;&#x2F;h2&gt;
&lt;p&gt;Use static analysis for what it&#x27;s good at. Build complete call graphs, trace execution paths, find every method that touches a piece of data. Deterministic, exhaustive, boring. Then use LLMs for what they&#x27;re good at, which is adding semantic meaning, business context and human-readable descriptions, so you get &quot;This method handles premium user sorting&quot; instead of &quot;this method takes a List and returns a List&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;Basically preprocess the codebase into a semantic map an LLM can actually navigate, with the hard work already done.&lt;&#x2F;p&gt;
&lt;p&gt;The raw idea is that you drop an annotation or comment on an entry point method like&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;@anchor
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;&#x2F;user&#x2F;compute-something-complicated
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and the tool does the traversal while the LLM does the enhancing. On the next prompt, the LLM looks at that generated file first.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Block vs stream ciphers</title>
        <published>2026-01-13T00:00:00+00:00</published>
        <updated>2026-01-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/12-block-vs-stream-ciphers/"/>
        <id>https://buenomini.com/posts/12-block-vs-stream-ciphers/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/12-block-vs-stream-ciphers/">&lt;p&gt;Adding encryption to my key-value store, I had to learn the difference between block and stream ciphers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;block-ciphers&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#block-ciphers&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Block ciphers&lt;&#x2F;h2&gt;
&lt;p&gt;A block cipher processes data in fixed-size chunks and encrypts one block at a time. AES is the common one. Its block size is 128 bits (16 bytes), so it takes 16 bytes of plaintext, outputs 16 bytes of ciphertext, and repeats that for each block.&lt;&#x2F;p&gt;
&lt;p&gt;The implementation is more complex, but there&#x27;s hardware support (AES-NI). Without hardware acceleration it&#x27;s slower, and a poor implementation can be vulnerable to side-channel attacks.&lt;&#x2F;p&gt;
&lt;p&gt;And if your data isn&#x27;t exactly 16 bytes (or a multiple of 16), you need padding.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stream-ciphers&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#stream-ciphers&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Stream ciphers&lt;&#x2F;h2&gt;
&lt;p&gt;A stream cipher encrypts data bit by bit or byte by byte. It generates a keystream and XORs it with your plaintext. ChaCha20 is the example here:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No fixed block size. Encrypt 5 bytes, get 5 bytes of ciphertext, no padding&lt;&#x2F;li&gt;
&lt;li&gt;Simpler design (only additions, XORs and rotations)&lt;&#x2F;li&gt;
&lt;li&gt;Fast in software without special hardware&lt;&#x2F;li&gt;
&lt;li&gt;Better for power consumption&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-i-m-using&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-i-m-using&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What I&#x27;m using&lt;&#x2F;h2&gt;
&lt;p&gt;ChaCha20, the stream cipher. Pure software implementation, simpler, faster without hardware acceleration. That&#x27;s it. Both are secure when used correctly, ChaCha20 just made more sense for a learning project where I&#x27;m not using hardware crypto.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Edge-triggered vs level-triggered epoll</title>
        <published>2026-01-06T00:00:00+00:00</published>
        <updated>2026-01-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/11-edge-vs-level-triggered/"/>
        <id>https://buenomini.com/posts/11-edge-vs-level-triggered/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/11-edge-vs-level-triggered/">&lt;p&gt;When you build an event loop with epoll (or kqueue), you have to pick a notification mode. It changes how the loop behaves and how much code you need to get it right.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;level-triggered-this-file-descriptor-is-ready&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#level-triggered-this-file-descriptor-is-ready&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Level-triggered: &quot;this file descriptor IS ready&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;Level-triggered is the default, and the simpler mode. You get notified every time you call &lt;code&gt;epoll_wait()&lt;&#x2F;code&gt; while data is available.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c z-code&quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; 100 bytes arrive on socket
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt;&lt;span class=&quot;z-entity z-name z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket &lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;ready&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;still &lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes left&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you don&#x27;t read all the data, the next &lt;code&gt;epoll_wait()&lt;&#x2F;code&gt; just tells you again. It&#x27;s forgiving.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;edge-triggered-this-file-descriptor-became-ready&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#edge-triggered-this-file-descriptor-became-ready&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Edge-triggered: &quot;this file descriptor BECAME ready&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;Edge-triggered only notifies you when the state changes from &quot;not ready&quot; to &quot;ready&quot;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c z-code&quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; 100 bytes arrive on socket
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt;&lt;span class=&quot;z-entity z-name z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;blocks forever &lt;span class=&quot;z-keyword z-operator z-arithmetic z-c&quot;&gt;-&lt;&#x2F;span&gt; no notification&lt;span class=&quot;z-keyword z-operator z-arithmetic z-c&quot;&gt;!&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So if you don&#x27;t read all the data, you won&#x27;t hear about it again until NEW data arrives, and the 50 bytes you left behind just sit there. That&#x27;s the gotcha. You have to drain the socket completely every time:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c z-code&quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-keyword z-control z-c&quot;&gt;while&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-comparison z-c&quot;&gt;!=&lt;&#x2F;span&gt; EAGAIN&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-block z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-c&quot;&gt;{&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Read until socket is exhausted
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-block z-c&quot;&gt;    &lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; process data
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-block z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-c&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;EAGAIN&lt;&#x2F;code&gt; means &quot;would block&quot;, the socket is empty and there&#x27;s no more data. Only then is it safe to go back to &lt;code&gt;epoll_wait()&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Same 100 bytes, side by side. Level-triggered:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c z-code&quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Partial read is fine
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt;&lt;span class=&quot;z-entity z-name z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready  &lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Notified again
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Edge-triggered, done wrong:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c z-code&quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Partial read
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt;&lt;span class=&quot;z-entity z-name z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-parameters z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-c&quot;&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;deadlock &lt;span class=&quot;z-keyword z-operator z-arithmetic z-c&quot;&gt;-&lt;&#x2F;span&gt; no notification&lt;span class=&quot;z-punctuation z-separator z-c&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;50&lt;&#x2F;span&gt; bytes stuck&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Edge-triggered, done right:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c z-code&quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;epoll_wait&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-accessor z-c&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; socket ready
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-keyword z-control z-c&quot;&gt;while&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;n &lt;span class=&quot;z-keyword z-operator z-assignment z-c&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;read&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;buf&lt;span class=&quot;z-punctuation z-separator z-c&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-word z-c&quot;&gt;sizeof&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;buf&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-comparison z-c&quot;&gt;&amp;gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-c&quot;&gt;0&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-block z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-c&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-block z-c&quot;&gt;    &lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-variable z-function z-c&quot;&gt;process&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-c&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;buf&lt;span class=&quot;z-punctuation z-separator z-c&quot;&gt;,&lt;&#x2F;span&gt; n&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-c&quot;&gt;&lt;span class=&quot;z-meta z-group z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-c&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-c&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-meta z-block z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-c&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-c&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-c&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-c&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Loop exits when read() returns EAGAIN
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;so-why-would-you-pick-edge-triggered&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#so-why-would-you-pick-edge-triggered&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
So why would you pick edge-triggered?&lt;&#x2F;h2&gt;
&lt;p&gt;Efficiency. Level-triggered can cause spurious wakeups, where you get notified again and again for the same data, and at scale (thousands of connections) that matters. Edge-triggered means fewer calls to &lt;code&gt;epoll_wait()&lt;&#x2F;code&gt; and no redundant notifications, which is better for high-throughput servers.&lt;&#x2F;p&gt;
&lt;p&gt;With edge-triggered you have to drain the socket completely, if not you block, and it&#x27;s a bit more complex on error handling. But I would guess if you&#x27;re building your own event loop you do care about efficiency.&lt;&#x2F;p&gt;
&lt;p&gt;Level-triggered is simpler logically. You can read part of the data and come back later, at the cost of more syscalls and slightly worse efficiency, and for learning or low-scale stuff the difference doesn&#x27;t matter.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-i-m-using&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-i-m-using&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What I&#x27;m using&lt;&#x2F;h2&gt;
&lt;p&gt;Level-triggered, for my key-value store learning project. The performance hit is ~10-20% at massive scale, which I&#x27;ll never reach, so the simplicity is worth it for a toy project. If I were building a production proxy server handling 100k+ connections, edge-triggered would matter. Not that I would ever build my own event loop for production, I&#x27;m not that crazy.&lt;&#x2F;p&gt;
&lt;p&gt;All of this applies to kqueue (macOS&#x2F;BSD) too, with different terminology: the &lt;code&gt;EV_CLEAR&lt;&#x2F;code&gt; flag gives you edge-triggered and the default behavior is level-triggered. Same concepts, different API.&lt;&#x2F;p&gt;
&lt;p&gt;Either way, don&#x27;t cargo-cult edge-triggered just because it&#x27;s &quot;more efficient&quot;. The complexity might not be worth it for your workload.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>IPC vs RPC</title>
        <published>2025-12-16T00:00:00+00:00</published>
        <updated>2025-12-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/10-ipc-vs-rpc/"/>
        <id>https://buenomini.com/posts/10-ipc-vs-rpc/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/10-ipc-vs-rpc/">&lt;p&gt;IPC (inter-process communication) and RPC (remote procedure call) solve the same problem, getting processes to talk to each other, just at very different levels of abstraction.&lt;&#x2F;p&gt;
&lt;p&gt;IPC is the low-level mechanism. You open connections, send messages and receive responses yourself, over sockets (TCP, UDP, Unix domain sockets), pipes, message queues or shared memory. And you&#x27;re in charge of everything: connection management, serialization, error handling, protocol design.&lt;&#x2F;p&gt;
&lt;p&gt;RPC is built on top of IPC (usually sockets) and makes remote calls look like local function calls:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Looks like a local function call
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; result &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; remote_server&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;getUserData&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;user_id&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Underneath, the function call gets serialized to a message, sent via a socket (IPC), executed remotely, and the result comes back, gets deserialized and is returned to the caller. You pretend it&#x27;s local.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-that-looks-like-for-a-key-value-store&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-that-looks-like-for-a-key-value-store&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What that looks like for a key-value store&lt;&#x2F;h2&gt;
&lt;p&gt;The IPC version is what I&#x27;m building. A CLI tool you run like &lt;code&gt;zkv connect --addr 127.0.0.1:6379&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash z-code&quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-shell&quot;&gt; zkv connect&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;Connected&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-shell&quot;&gt; to zkv server&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-keyword z-operator z-assignment z-redirection z-shell&quot;&gt;&amp;gt;&lt;&#x2F;span&gt; SET &lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;mykey&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function-call z-arguments z-shell&quot;&gt; myvalue&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;OK&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-keyword z-operator z-assignment z-redirection z-shell&quot;&gt;&amp;gt;&lt;&#x2F;span&gt; GET &lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;mykey&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-shell z-bash&quot;&gt;&lt;span class=&quot;z-meta z-function-call z-shell&quot;&gt;&lt;span class=&quot;z-variable z-function z-shell&quot;&gt;myvalue&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What happens:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;CLI opens a TCP socket connection&lt;&#x2F;li&gt;
&lt;li&gt;User types &lt;code&gt;SET key value&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;CLI encodes it into wire protocol format&lt;&#x2F;li&gt;
&lt;li&gt;Sends bytes over socket&lt;&#x2F;li&gt;
&lt;li&gt;Receives response bytes&lt;&#x2F;li&gt;
&lt;li&gt;Decodes and prints response&lt;&#x2F;li&gt;
&lt;li&gt;Connection stays open for more commands&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;I manage the socket, the protocol and the encoding&#x2F;decoding. All of it.&lt;&#x2F;p&gt;
&lt;p&gt;The RPC version would be a library you include in your code:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-keyword z-other z-rust&quot;&gt;use&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-path z-rust&quot;&gt;zkv&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;Client&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; client &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-path z-rust&quot;&gt;Client&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;connect&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;127.0.0.1:6379&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;?&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;client&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function z-rust&quot;&gt;set&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;mykey&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;myvalue&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;?&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; value &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; client&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function z-rust&quot;&gt;get&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;mykey&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;?&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The library establishes the socket connection internally, &lt;code&gt;client.set()&lt;&#x2F;code&gt; gets serialized to the protocol format and sent over the socket, the response is deserialized and returned as a typed value, and the library handles the connection management. You never see any of it, you just call functions.&lt;&#x2F;p&gt;
&lt;p&gt;The usual examples are gRPC (define services in Protocol Buffers, generate client&#x2F;server code, call methods, and it runs on HTTP&#x2F;2 over TCP sockets), JSON-RPC (HTTP requests with JSON payloads that map to function calls) and Apache Thrift, which is similar to gRPC with a different IDL and wire protocol.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;which-one-to-use&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#which-one-to-use&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Which one to use&lt;&#x2F;h2&gt;
&lt;p&gt;IPC when you want full control over the protocol, when you&#x27;re building a CLI tool or custom client, when it&#x27;s a performance-critical path and you don&#x27;t want the abstraction overhead, or when you want to learn how things work underneath (that&#x27;s me).&lt;&#x2F;p&gt;
&lt;p&gt;RPC when you want simple function-call semantics, when you&#x27;re building a library for others to use, when type safety matters (generated code from schemas) or when you need multiple languages. gRPC has 10+ language bindings.&lt;&#x2F;p&gt;
&lt;p&gt;So RPC trades control for convenience. You get the simple API, type safety from a schema, and connection pooling, retries and timeouts handled for you. You give up control over the wire format, visibility into what&#x27;s happening on the network, and some performance to the extra layers. For most applications that&#x27;s a good deal I think. For learning systems programming, IPC teaches you more.&lt;&#x2F;p&gt;
&lt;p&gt;My zkv project is IPC-based, a CLI that manages TCP sockets directly, and with it I&#x27;m learning socket programming, wire protocol design, message framing and connection handling. Could I wrap it in an RPC library later? Sure. But if you learn the IPC layer first RPC stops being magic, and if you start with RPC you might never understand what&#x27;s happening on the wire.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Wire protocol framing (and why it prevents attacks)</title>
        <published>2025-12-09T00:00:00+00:00</published>
        <updated>2025-12-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/09-wire-protocol-framing/"/>
        <id>https://buenomini.com/posts/09-wire-protocol-framing/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/09-wire-protocol-framing/">&lt;p&gt;Building a wire protocol for my key-value store, I hit this immediately: how the hell do you know where one message ends and the next begins?&lt;&#x2F;p&gt;
&lt;p&gt;TCP gives you a stream of bytes. Just bytes. One &lt;code&gt;read()&lt;&#x2F;code&gt; might give you half a message, two messages smooshed together, or one and a half, and without framing you can&#x27;t parse anything.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem-with-actual-suffering&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-problem-with-actual-suffering&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The problem (with actual suffering)&lt;&#x2F;h2&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Client sends: &amp;quot;SET key value&amp;quot;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Server receives: &amp;quot;SET k&amp;quot; ... &amp;quot;ey val&amp;quot; ... &amp;quot;ue&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You don&#x27;t know when you have the complete message... My first version just read until it had enough data that looked like a command. Worked great in my toy test, broke immediately when I tried pipelining requests.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;text-framing-options&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#text-framing-options&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Text framing options&lt;&#x2F;h2&gt;
&lt;p&gt;The delimiter approach ends each message with &lt;code&gt;\n&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;SET key value\n
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;GET key\n
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Simple and human-readable, you can &lt;code&gt;telnet&lt;&#x2F;code&gt; in and type commands. But what if the value contains &lt;code&gt;\n&lt;&#x2F;code&gt;? Now you need escaping.&lt;&#x2F;p&gt;
&lt;p&gt;Or length-prefixed, where you say how long each piece is:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is how Redis (the RESP protocol) works. &lt;code&gt;$7&lt;&#x2F;code&gt; means &quot;7 bytes follow&quot;. No escaping and it handles binary data. Less readable, but way harder to break.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;binary-framing-the-fast-way&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#binary-framing-the-fast-way&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Binary framing (the fast way)&lt;&#x2F;h2&gt;
&lt;p&gt;The first 4 bytes are the message length, the rest is payload:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;[0x00, 0x00, 0x00, 0x0A, ...10 bytes of payload...]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Read 4 bytes, parse the length, then read exactly that many bytes.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-member z-zig&quot;&gt;len_buf&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; [&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;4&lt;&#x2F;span&gt;]&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-zig&quot;&gt;undefined&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; socket.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readAll&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;len_buf&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; msg_len &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; std.mem.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readInt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u32&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;len_buf&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-language z-enum&quot;&gt; .big&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Network byte order
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; buffer &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; allocator.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;alloc&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; msg_len&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; socket.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readAll&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;buffer&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Efficient, handles any size. There&#x27;s also Type-Length-Value (TLV), where each field has a type byte, a length, then the data. More overhead but self-describing, which was a bit overkill for me.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-matters-for-security-or-how-i-learned-attackers-are-creative&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#why-this-matters-for-security-or-how-i-learned-attackers-are-creative&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Why this matters for security (or: how I learned attackers are creative)&lt;&#x2F;h2&gt;
&lt;p&gt;Without proper framing there are a few ways to get wrecked. A buffer overflow is the classic one, the server keeps reading into a fixed buffer until it overflows and you get a crash or worse. This is 90s-era exploitation but people still get it wrong.&lt;&#x2F;p&gt;
&lt;p&gt;Memory exhaustion is even easier. Just claim the message is 4GB:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;[LENGTH: 0xFFFFFFFF, ...]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The server goes &quot;okay cool, let me allocate 4GB&quot;, runs out of memory and crashes. Fun for the attacker, less fun for you at 2am.&lt;&#x2F;p&gt;
&lt;p&gt;And then Slowloris, which sends data 1 byte at a time and never completes the message:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Send: &amp;quot;S&amp;quot;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Wait 10 seconds
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Send: &amp;quot;E&amp;quot;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Wait 10 seconds
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;...
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The server keeps the connection open waiting for the rest, the connection pool runs out and nobody else can connect. Denial of service with minimal bandwidth.&lt;&#x2F;p&gt;
&lt;p&gt;The fix is length-prefixed framing with validation:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-member z-zig&quot;&gt;len_buf&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; [&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;4&lt;&#x2F;span&gt;]&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-zig&quot;&gt;undefined&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; socket.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readAll&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;len_buf&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; msg_len &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; std.mem.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readInt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u32&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;len_buf&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-language z-enum&quot;&gt; .big&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Validate BEFORE allocating (important!)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;if&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;msg_len &lt;span class=&quot;z-keyword z-operator z-logical z-zig&quot;&gt;&amp;gt;&lt;&#x2F;span&gt; MAX_MESSAGE_SIZE&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-error z-zig&quot;&gt;error&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-zig&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-error z-zig&quot;&gt;MessageTooLarge&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Safe to allocate - we know the size
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; buffer &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; allocator.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;alloc&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; msg_len&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Read exactly msg_len bytes with timeout
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; socket.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readAllWithTimeout&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;buffer&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; TIMEOUT&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now you can reject oversized messages, allocate exactly what you need, time out slow clients, and know when the message is complete. Revolutionary, I know.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-actual-implementation-with-mistakes&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#my-actual-implementation-with-mistakes&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
My actual implementation (with mistakes)&lt;&#x2F;h2&gt;
&lt;p&gt;Initially? Just read until &lt;code&gt;\n&lt;&#x2F;code&gt;. Worked fine for simple tests where I typed the commands by hand. Then, staring at my code at midnight, I realized three things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;What if a value contains &lt;code&gt;\n&lt;&#x2F;code&gt;? (spoiler: it will)&lt;&#x2F;li&gt;
&lt;li&gt;What if someone sends a 10GB message? (my laptop has 16GB of RAM and I like using some of it)&lt;&#x2F;li&gt;
&lt;li&gt;How do I handle partial reads? (TCP doesn&#x27;t give a shit about your message boundaries)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So I switched to length-prefixed:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Read 4-byte length header
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-member z-zig&quot;&gt;len_buf&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; [&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;4&lt;&#x2F;span&gt;]&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-zig&quot;&gt;undefined&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; socket.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readAll&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;len_buf&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; msg_len &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; std.mem.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readInt&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u32&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;len_buf&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-language z-enum&quot;&gt; .big&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Validate BEFORE allocating
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;if&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;msg_len &lt;span class=&quot;z-keyword z-operator z-logical z-zig&quot;&gt;&amp;gt;&lt;&#x2F;span&gt; MAX_MSG_SIZE&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-error z-zig&quot;&gt;error&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-zig&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-error z-zig&quot;&gt;MessageTooLarge&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Now safe to allocate and read
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; buffer &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; allocator.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;alloc&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; msg_len&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; socket.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;readAll&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;buffer&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Simple, safe, handles binary data. The kind of code I should&#x27;ve written first but didn&#x27;t because I was too clever (read: lazy).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;text-vs-binary-the-eternal-debate&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#text-vs-binary-the-eternal-debate&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Text vs binary (the eternal debate)&lt;&#x2F;h2&gt;
&lt;p&gt;For my learning project I&#x27;m using a text-based protocol with length-prefixed framing. Redis does this (RESP with length prefixes), HTTP does this (&lt;code&gt;Content-Length&lt;&#x2F;code&gt; headers), both work fine. Binary protocols like Protocol Buffers are more compact and faster to parse, but for a toy KV store handling thousands of ops&#x2F;sec, nowhere near millions, the difference is negligible. And I can &lt;code&gt;telnet&lt;&#x2F;code&gt; in to debug, which is worth the performance hit (that I&#x27;ll never actually hit).&lt;&#x2F;p&gt;
&lt;p&gt;Whatever you pick, validate the message size before you allocate, and make sure you can tell when a message is complete. Binary data support only if you need it. Everything else is preference.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;I first learned about framing (the hard way) while building &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;01-hush&#x2F;&quot;&gt;hush&lt;&#x2F;a&gt;, a P2P secret sharing tool where trusting arbitrary peers is... optimistic.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Why your web service doesn&#x27;t need Rust (probably)</title>
        <published>2025-12-02T00:00:00+00:00</published>
        <updated>2025-12-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/08-systems-languages-web-services/"/>
        <id>https://buenomini.com/posts/08-systems-languages-web-services/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/08-systems-languages-web-services/">&lt;p&gt;I dislike Java.&lt;&#x2F;p&gt;
&lt;p&gt;The verbosity, the &lt;code&gt;AbstractFactoryPatternManagerImpl&lt;&#x2F;code&gt; nonsense. Every time I write Java I think it would be less verbose to write another blog post.&lt;&#x2F;p&gt;
&lt;p&gt;Zig and Rust are &lt;em&gt;right there&lt;&#x2F;em&gt;, looking at me, constantly. Manual memory management, zero-cost abstractions, no garbage collection pauses, I could write code that runs circles around the JVM. So naturally, while working on a Java microservice, I started thinking about how I could justify bringing a systems language like those two into this ecosystem.&lt;&#x2F;p&gt;
&lt;p&gt;Spoiler: I could not. But &lt;em&gt;why&lt;&#x2F;em&gt; I could not is a pretty good example of real world constraints beating theoretical performance.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-sexy-proposition&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-sexy-proposition&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The sexy proposition&lt;&#x2F;h2&gt;
&lt;p&gt;Systems languages promise bare-metal performance. Talk to the kernel, control every byte, way past what a well-tuned JVM or a &quot;good enough for CRUD&quot; service gives you. And when you&#x27;re maintaining a service that feels slow and you&#x27;re like me (you don&#x27;t shy away from learning something new as long as it makes sense), that&#x27;s intoxicating. You start manufacturing opportunities everywhere:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;This XML parser could be 3x faster in Zig!&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&quot;These protobuf codecs are doing unnecessary allocations!&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&quot;We could rewrite the database layer and eliminate JNI overhead!&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Sadly, it isn&#x27;t that simple unless you&#x27;re reckless. You need to do the fucking math first, and that&#x27;s before the hard sell of adopting a new technology you&#x27;ll have to maintain. New pipelines, new monitoring, a lot of new things.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-case-study-a-java-microservice&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-case-study-a-java-microservice&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The case study: a Java microservice&lt;&#x2F;h2&gt;
&lt;p&gt;This is a real service. A distributed user data service on a Java&#x2F;JVM stack, gRPC with some REST API sprinkles, database operations, message queues. It handles tens of thousands of requests per second. It&#x27;s I&#x2F;O-bound (network, database, external APIs), the latency targets are in the tens to hundreds of milliseconds, and it already uses async patterns.&lt;&#x2F;p&gt;
&lt;p&gt;I went looking for places to drop in some Rust or Zig and found three candidates:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The XML&#x2F;TTML parser. It parses files, and native code could make it 2-3x faster, which is about 5-10ms in absolute terms. It isn&#x27;t on the critical path anyway.&lt;&#x2F;li&gt;
&lt;li&gt;The protocol buffer codecs. 400+ lines of serialization logic, already on binary protobuf (efficient). Potential gains of under 5ms, against 10-100ms of network latency.&lt;&#x2F;li&gt;
&lt;li&gt;The database access layer. This is where it gets interesting.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;the-sad-truth&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-sad-truth&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The sad truth&lt;&#x2F;h2&gt;
&lt;p&gt;The service uses a distributed database that&#x27;s already written in C++. Every database operation goes through this flow:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Java Service Layer
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;      ↓ (JNI - ~100-200ns overhead)
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;JVM Bindings (Java wrapper)
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;      ↓
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Native C Library
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;      ↓ (Network - ~1-5ms latency)
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Database Server (C++)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You know what I thought? &quot;If we wrote the data access layer in Rust, we could talk directly to that C library! We&#x27;d eliminate the JNI overhead and I for sure can make it safer&quot; (the latter one is of course delusional).&lt;&#x2F;p&gt;
&lt;p&gt;So I did the math. Single-key lookups are the most common operation, and they take 1-5ms. JNI adds 100-200ns to that, so 0.01-0.02% of the total. Range queries (10-100 items) take 5-20ms, same 100-200ns of JNI, which is 0.001-0.004%.&lt;&#x2F;p&gt;
&lt;p&gt;Even if I made JNI infinitely fast I&#x27;d improve overall latency by less than 2%, because the network latency to the database (1-5ms) is 5,000 to 50,000 times larger than the JNI overhead.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;two-orders-of-magnitude-or-don-t-mention-it&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#two-orders-of-magnitude-or-don-t-mention-it&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Two orders of magnitude or don&#x27;t mention it&lt;&#x2F;h2&gt;
&lt;p&gt;My new rule: only optimize a component if it&#x27;s within two orders of magnitude of your dominant cost.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;JNI overhead: 100-200ns&lt;&#x2F;li&gt;
&lt;li&gt;Network latency: 1-5ms (5,000-50,000ns)&lt;&#x2F;li&gt;
&lt;li&gt;Difference: 3-4 orders of magnitude&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So JNI is not worth touching, and the same goes for everything else. Why would I ever start by optimizing anything other than the bottleneck...&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-yes&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#when-yes&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
When yes?&lt;&#x2F;h2&gt;
&lt;p&gt;Look, I&#x27;m not saying systems languages are useless for web services. They make sense when you have CPU-intensive operations at massive scale: millions of ops&#x2F;sec (this service does thousands), computational kernels called constantly (crypto, compression, encoding), data processing pipelines without I&#x2F;O waits. Or memory-sensitive workloads, where GC pauses directly hurt user experience, you need sub-millisecond latency (&amp;lt;1ms p99) and every microsecond matters.&lt;&#x2F;p&gt;
&lt;p&gt;You might be able to build a case if you already have a custom proxy server, a cache layer, an in-memory analytics engine or stuff like video encoding, I guess. None of those are waiting on I&#x2F;O. A typical CRUD service hitting databases and external APIs is I&#x2F;O-bound, and there the language doesn&#x27;t matter.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-sadder-truth&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-sadder-truth&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The sadder truth&lt;&#x2F;h2&gt;
&lt;p&gt;99% of developers (including me) don&#x27;t have problems that systems languages solve.&lt;&#x2F;p&gt;
&lt;p&gt;Your web service is slow because your database queries suck, or because you&#x27;re not caching properly, or because you&#x27;re underusing async. If I had a penny for every time I found multiple awaits in a row instead of one await-all, I would have like 2 dollars more in my bank account. It&#x27;s layer 8, or it&#x27;s bound to slow I&#x2F;O. Java&#x27;s garbage collection has nothing to do with it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;imagine-you-are-in-that-1-where-you-need-it&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#imagine-you-are-in-that-1-where-you-need-it&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Imagine you are in that 1% where you need it&lt;&#x2F;h2&gt;
&lt;p&gt;Okay, say all the planets have aligned. You&#x27;ve profiled, you&#x27;ve done the math, a systems language makes sense, you have CPU-bound operations at massive scale and the 10x improvement is real.&lt;&#x2F;p&gt;
&lt;p&gt;You still need to put in the work to make it fast and not shoot yourself in the foot. Rust won&#x27;t save you from terrible algorithms, Zig won&#x27;t fix your O(n²) nested loops, and both will happily let you write slow code if you&#x27;re not careful (btw, if that sounds scary, your current code could use a revision).&lt;&#x2F;p&gt;
&lt;p&gt;Then there&#x27;s the organizational battle. You need to fight for adopting something entirely new in a larger organization, and onboarding a new language is never as easy as installing the compiler and creating a CI&#x2F;CD pipeline. Split language expertise, split codebase, hiring gets harder, and either everyone context switches (annoying) or you end up with knowledge silos in the team.&lt;&#x2F;p&gt;
&lt;p&gt;You may not struggle with platform-specific binaries, but what about libraries? Does the obscure SaaS your company forces you to use for performance metrics have a good library for your new Rust server? Or will you have to build it yourself to get the minimum observability in prod you need to even measure whether all this was worth it?&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;d still probably say &quot;only introduce platform complexity if it gives you a 10x improvement&quot;, or something like that. For this I&#x2F;O-bound service Rust might give 1.1-1.2x on the compute portions, and compute is under 10% of request time. So under 2% overall, and that&#x27;s not worth doubling operational complexity.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-reflection&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#my-reflection&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
My reflection&lt;&#x2F;h2&gt;
&lt;p&gt;I wanted to use Zig and Rust because they&#x27;re &lt;em&gt;cool&lt;&#x2F;em&gt;, because systems programming feels like &quot;real&quot; engineering, and because I was bored of Java. Those are preferences.&lt;&#x2F;p&gt;
&lt;p&gt;Do the math first. Profile before you rewrite, ask what&#x27;s slow before you ask what could be faster. The network and the database take the same time whether you call them from Java or Rust, and external APIs respond just as slowly to your beautiful systems programming code.&lt;&#x2F;p&gt;
&lt;p&gt;Choose boring, proven technology that matches your problem, and save the systems programming energy for a toy project. You&#x27;ll enjoy it more and it won&#x27;t be tainted by your job requirements.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust vs Zig: the ecosystem maturity gap</title>
        <published>2025-11-30T00:00:00+00:00</published>
        <updated>2025-11-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/07-rust-vs-zig-comparison/"/>
        <id>https://buenomini.com/posts/07-rust-vs-zig-comparison/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/07-rust-vs-zig-comparison/">&lt;p&gt;I&#x27;m building the same encrypted key-value store in Rust and Zig at the same time, module by module, feature by feature. The goal was a fair comparison, and to learn both languages by solving identical problems in parallel.&lt;&#x2F;p&gt;
&lt;p&gt;Four modules in, my intermediate verdict is that I don&#x27;t want to go back to Zig as much as I want to go back to Rust. Which surprised me, I went in expecting the exact opposite.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-i-expected&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-i-expected&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What I expected&lt;&#x2F;h2&gt;
&lt;p&gt;Zig: simple language, closer to C, full control. Manual memory management means you learn how things really work.&lt;&#x2F;p&gt;
&lt;p&gt;Rust: complex language, steep learning curve. Ownership, borrowing, lifetimes. Everyone complains about the borrow checker. Lots of syntax to learn.&lt;&#x2F;p&gt;
&lt;p&gt;I thought Zig&#x27;s simplicity would make it easier to be productive. I was wrong.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;module-1-storage-layer-hashmap-wrapper&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#module-1-storage-layer-hashmap-wrapper&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Module 1: storage layer (HashMap wrapper)&lt;&#x2F;h2&gt;
&lt;p&gt;In Zig the code itself was straightforward, a simple wrapper around &lt;code&gt;std.StringHashMap&lt;&#x2F;code&gt;. The REAL challenge was understanding memory management, the hashmap internals, memory bugs, testing for memory leaks, etc. (more on this in &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;03-zig-hashmap-memory&#x2F;&quot;&gt;the Zig hashmap gotcha&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;04-stack-vs-heap-zig&#x2F;&quot;&gt;stack vs heap memory&lt;&#x2F;a&gt;). The language concepts were &lt;em&gt;easy&lt;&#x2F;em&gt;, but the systems knowledge it required was harder than I thought, and I spent more time learning ABOUT memory than writing code. Don&#x27;t get me wrong, that&#x27;s an amazing thing and really speaks volumes about Zig! The language got out of my way immediately, I just had to deeply understand what was happening underneath.&lt;&#x2F;p&gt;
&lt;p&gt;Rust was the opposite problem. The code took longer to write because I had to learn &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;02-string-vs-str-rust&#x2F;&quot;&gt;String vs &amp;amp;str&lt;&#x2F;a&gt; and language specifics like mutability, Option, Result, etc. I didn&#x27;t think &lt;em&gt;that&lt;&#x2F;em&gt; much about memory management because I could rely on the compiler shouting at me (it probably also matters that I had just built this in Zig, where I managed memory by hand and suffered some use-after-free bugs). The concepts were &lt;em&gt;a bit more complex&lt;&#x2F;em&gt;, but not really that bad.&lt;&#x2F;p&gt;
&lt;p&gt;No allocators, no manual freeing, no use-after-free possible. I fought the type system instead, but once it compiled, it was ok.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;module-2-wire-protocol-parser-serializer&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#module-2-wire-protocol-parser-serializer&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Module 2: wire protocol (parser&#x2F;serializer)&lt;&#x2F;h2&gt;
&lt;p&gt;Rust enums are amazing, each variant can hold different data types &amp;lt;3. Combined with pattern matching, parsing commands was clean and type-safe. And &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; for explicit error handling are so nice. Everyone should do this. Why doesn&#x27;t every language do this? Module 2 wasn&#x27;t as hard as I expected, Rust&#x27;s type system made the parser straightforward.&lt;&#x2F;p&gt;
&lt;p&gt;Zig&#x27;s tagged unions work a lot like Rust&#x27;s enums with data, and I built a parser that turns text commands into typed &lt;code&gt;ParsedCommand&lt;&#x2F;code&gt; union variants. Different syntax, but the concept clicked.&lt;&#x2F;p&gt;
&lt;p&gt;Then I found a memory leak pattern. Some responses returned string literals like &lt;code&gt;&quot;OK&quot;&lt;&#x2F;code&gt;, while error responses used &lt;code&gt;allocPrint()&lt;&#x2F;code&gt;, which allocates, and the caller didn&#x27;t know which responses needed freeing. I ended up passing a fixed-size buffer to avoid the allocation (a waste of resources for simple error strings, but at least it&#x27;s consistent). In Rust I didn&#x27;t have to think about any of this, the type system handles it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;module-3-tcp-server-where-the-gap-showed-up&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#module-3-tcp-server-where-the-gap-showed-up&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Module 3: TCP server, where the gap showed up&lt;&#x2F;h2&gt;
&lt;p&gt;So far it was ok, I liked some things from each and I mostly liked both. But this is where that changed.&lt;&#x2F;p&gt;
&lt;p&gt;In Zig I spent an hour fighting the package system trying to install &lt;code&gt;clap&lt;&#x2F;code&gt; for CLI argument parsing. Eventually I gave up and wrote the arg parsing by hand.&lt;&#x2F;p&gt;
&lt;p&gt;Then I needed to read user input from stdin, but &lt;code&gt;std.io.getStdIn()&lt;&#x2F;code&gt; doesn&#x27;t exist in my version of Zig. I had to dig through the actual library code to figure out it&#x27;s &lt;code&gt;std.fs.File.stdin()&lt;&#x2F;code&gt; now. A lot of guesswork involved. APIs keep changing between versions with no migration guide, and TCP worked eventually, but even basic things like printing to the console needed workarounds. I spent 6-8 hours on this module, mostly fighting tooling instead of writing code.&lt;&#x2F;p&gt;
&lt;p&gt;In Rust everything just worked. &lt;code&gt;TcpListener::bind()&lt;&#x2F;code&gt; and &lt;code&gt;.accept()&lt;&#x2F;code&gt; did what they said on the tin. The documentation was clear and accurate. Need to print something? &lt;code&gt;println!(&quot;Connected to {}:{}&quot;, host, port)&lt;&#x2F;code&gt;. Done. Buffers were straightforward: &lt;code&gt;let mut buffer: [u8; 1024] = [0; 1024];&lt;&#x2F;code&gt;. Error handling with the &lt;code&gt;?&lt;&#x2F;code&gt; operator makes propagation trivial, and Cargo makes dependency management painless.&lt;&#x2F;p&gt;
&lt;p&gt;I spent 2-3 hours on this module, actually implementing features.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;module-4-event-loop&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#module-4-event-loop&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Module 4: event loop&lt;&#x2F;h2&gt;
&lt;p&gt;I built an I&#x2F;O multiplexing event loop with &lt;code&gt;mio&lt;&#x2F;code&gt; in Rust. The server now handles multiple concurrent connections on a single thread.&lt;&#x2F;p&gt;
&lt;p&gt;The hardest part was understanding registration. You register the TCP listener with a token, and when a client connects you register that connection with its own unique token. You keep a HashMap of &lt;code&gt;Token -&amp;gt; TcpStream&lt;&#x2F;code&gt;, so when poll tells you &quot;Token 5 has data ready&quot;, you look it up and handle it. Once that clicked the implementation was straightforward. &lt;code&gt;mio&lt;&#x2F;code&gt; wraps epoll (Linux) and kqueue (macOS), so I didn&#x27;t have to deal with platform-specific syscalls, and the API is clean: register sockets, poll for events, dispatch to handlers.&lt;&#x2F;p&gt;
&lt;p&gt;I haven&#x27;t implemented this in Zig yet. And I&#x27;m kinda scared.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;honestly&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#honestly&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Honestly...&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;ve been avoiding it, but I need to admit it: switching back to zkv to implement the event loop in Zig feels like a chore. I don&#x27;t want to do it.&lt;&#x2F;p&gt;
&lt;p&gt;In Rust I feel productive. Things work, I make progress. I learn something, implement it, it compiles, it runs. In Zig every task feels like a fight, either with the package manager, missing documentation or APIs that changed between versions. It&#x27;s quite some work (for me) to figure out basic things that should be simple. Matklad has a word for this, &quot;unfinishedness&quot;, and his point is that it&#x27;s a different thing from immaturity.&lt;&#x2F;p&gt;
&lt;p&gt;Both languages teach systems programming, just different parts of it. Zig makes you understand memory management deeply, Rust makes you understand ownership and borrowing deeply.&lt;&#x2F;p&gt;
&lt;p&gt;I came in expecting Rust to be harder than Zig (which in a way is true). But Zig&#x27;s immaturity hurts more than Rust&#x27;s ownership, borrowing and lifetimes. I&#x27;d rather fight a strict compiler than missing documentation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-now&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-now&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What now?&lt;&#x2F;h2&gt;
&lt;p&gt;Doing both at once gave me the fair comparison I wanted. What I didn&#x27;t expect is how much ecosystem maturity matters.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ll finish rkv completely in Rust first, then decide if I want to go back to zkv. Or I might just accept that this became a Rust learning project, and that&#x27;s okay. There&#x27;s still a space in my heart for Zig, and I think I&#x27;ll give it another serious try when they &lt;a href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=f30PceqQWko&quot;&gt;re-release async await&lt;&#x2F;a&gt;. It looks amazing.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;&#x2F;strong&gt; Writing posts like this is pretty easy when you keep proper logs, more on that in &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;05-learning-framework&#x2F;&quot;&gt;my learning framework&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;&#x2F;strong&gt; This is based on Zig 0.15.2 (pre-1.0, constantly changing). Zig will mature, and the language itself is interesting. But right now, in 2025, Rust&#x27;s ecosystem is years ahead.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Knowledge vs experience in SWE</title>
        <published>2025-11-25T00:00:00+00:00</published>
        <updated>2025-11-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/06-knowledge-vs-experience/"/>
        <id>https://buenomini.com/posts/06-knowledge-vs-experience/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/06-knowledge-vs-experience/">&lt;p&gt;Ten years ago, I thought I was smart as f**k.&lt;&#x2F;p&gt;
&lt;p&gt;I could talk about database indexing strategies, distributed systems, scaling patterns. I&#x27;d read the papers, watched all the talks, followed the right people on Twitter. I was ready to architect the shit out of any system you threw at me. In theory.&lt;&#x2F;p&gt;
&lt;p&gt;People didn&#x27;t take me seriously and I was &lt;em&gt;so&lt;&#x2F;em&gt; confused. Like, didn&#x27;t they hear me explain CQRS at lunch? Did they miss my Slack message about event sourcing? I had opinions on everything. Strong ones.&lt;&#x2F;p&gt;
&lt;p&gt;Now I get it. I was knowledge-rich and experience-poor, and in software engineering that difference matters more than we admit.&lt;&#x2F;p&gt;
&lt;p&gt;The tech industry has a bad habit of conflating the two. We treat someone who&#x27;s read all the Kubernetes docs the same as someone who&#x27;s maintained production K8s clusters at scale, and we interview for knowledge (algorithm puzzles, system design on whiteboards) when we should hire for experience (has this person actually done the thing?). And neither juniors nor seniors are honest about this. Juniors overestimate what knowledge alone can do, seniors forget how much of what they can do comes from PTSD.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-they-are-in-my-humble-opinion&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-they-are-in-my-humble-opinion&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What they are, in my humble opinion&lt;&#x2F;h2&gt;
&lt;p&gt;Knowledge is what you learn from books, talks, documentation and courses. Understanding how a hash map works, knowing the theory of database indexing, being able to explain the CAP theorem at a dinner party.&lt;&#x2F;p&gt;
&lt;p&gt;Experience is actually building the thing. Maintaining APIs at 10,000 requests per second. Debugging a production incident at 3am when your DB index with tens of billions of documents is thrashing. Shipping code and living with the consequences.&lt;&#x2F;p&gt;
&lt;p&gt;My two cents: knowledge is useless until it&#x27;s applied. Learning for learning&#x27;s sake is a trap. I&#x27;m not against understanding theory, but theory without application is just trivia. Kinda like a dog that knows how to play dead but won&#x27;t come to you when you call it off-leash. You can watch 100 YouTube videos about distributed systems. You can memorize every CAP theorem tradeoff (had to google it just now). But until you&#x27;ve debugged a split-brain scenario in production, you don&#x27;t really &quot;&lt;em&gt;know&lt;&#x2F;em&gt;&quot; distributed systems.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-one-way-street&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-one-way-street&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The one-way street&lt;&#x2F;h2&gt;
&lt;p&gt;Experience creates knowledge. Sometimes explicitly (you learn a specific technique), sometimes through suffering (you learn what NOT to do after a 3 day outage).&lt;&#x2F;p&gt;
&lt;p&gt;But knowledge does NOT create experience, and this is where people get confused.&lt;&#x2F;p&gt;
&lt;p&gt;If you have to build a super scalable database and you can pick between:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Someone who knows a ton of database theory, has read all the papers, can explain B-trees and LSM-trees in detail (again, I had to google), but has never built a database&lt;&#x2F;li&gt;
&lt;li&gt;Someone who has built several databases already&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I&#x27;m picking the latter every single time. Not even close.&lt;&#x2F;p&gt;
&lt;p&gt;The person with experience has the knowledge PLUS all the things that can&#x27;t be taught:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The edge cases that only show up at scale (never on your laptop, always in prod, always on Friday at 5pm)&lt;&#x2F;li&gt;
&lt;li&gt;The tradeoffs that only matter in production (turns out &quot;eventual consistency&quot; means &quot;eventually someone will be pissed&quot;)&lt;&#x2F;li&gt;
&lt;li&gt;The unknown unknowns that will reveal themselves to you in the most spectacular fashion 7 months into the project that should&#x27;ve been delivered 6 months ago because you scoped it like shit thinking &quot;how hard could it be&quot; and now the database is on fire the API is returning 503s your manager is asking for a post-mortem you haven&#x27;t even written the mortem yet because the patient is still dying and you&#x27;re seriously considering just deleting your Slack and moving to a cabin in the woods my brother you are NOT recovering from this one&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;They might not remember every academic paper, but they know what actually breaks in the real world, and that&#x27;s worth infinitely more.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-humans-learn&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#how-humans-learn&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
How humans learn&lt;&#x2F;h2&gt;
&lt;p&gt;From what I&#x27;ve read, humans learn by doing, by practicing, by writing things in their own words. Understanding beats memorizing, and doing is the best way to understand a topic deeply (and it builds the kind of intuition you can&#x27;t get from a book).&lt;&#x2F;p&gt;
&lt;p&gt;A concrete example from my recent experience. I&#x27;ve been getting into low-level programming lately. I watched probably 10 YouTube videos about how I&#x2F;O multiplexing (event loops) works. I understood the concepts: epoll, kqueue, file descriptors, non-blocking I&#x2F;O. I could explain it to someone.&lt;&#x2F;p&gt;
&lt;p&gt;That was a complete waste of time compared to actually building an in-memory key-value store in Zig and Rust with 100% manual event loops using Linux and macOS syscalls. After the videos I had surface-level familiarity that would&#x27;ve been gone in a week. After building it, I understand:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Why event loops work this way&lt;&#x2F;li&gt;
&lt;li&gt;What the actual tradeoffs are&lt;&#x2F;li&gt;
&lt;li&gt;Where things break down&lt;&#x2F;li&gt;
&lt;li&gt;How to debug when things go wrong&lt;&#x2F;li&gt;
&lt;li&gt;The feel of working with these systems&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The videos gave me knowledge. Building gave me experience, which gave me deeper knowledge. And this holds everywhere, you can read about writing concurrent code, or you can debug a race condition for 6 hours and never make that mistake again (until you do). You can study memory management, or you can chase down a memory leak in production.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-experience-teaches-you&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-experience-teaches-you&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What experience teaches you&lt;&#x2F;h2&gt;
&lt;p&gt;There&#x27;s a concept called &quot;unknown unknowns&quot;: you don&#x27;t know what you don&#x27;t know. Books can&#x27;t teach you this, only experience uncovers it. It&#x27;s like legendary Pokémon, they&#x27;re not in the Pokédex.&lt;&#x2F;p&gt;
&lt;p&gt;When I started taking on genuinely big, complicated projects at massive scale at work, everything changed. Building APIs that handle tens of thousands of requests per second, managing database indices with tens of billions of documents, those taught me things no documentation or course or amount of studying could have prepared me for.&lt;&#x2F;p&gt;
&lt;p&gt;Books teach you clean examples with perfect abstractions and well-named variables. Production is a 7-year-old Django monolith with a God class called &lt;code&gt;Utils.py&lt;&#x2F;code&gt; that&#x27;s 3000 lines long, database migrations that were written by someone who quit in 2019 and left no documentation, a caching layer that sometimes works and sometimes just returns &lt;code&gt;null&lt;&#x2F;code&gt; for reasons nobody understands, and a comment that says &quot;TODO: fix this properly&quot; from 2021. You will work with this. You will ship features in this. The books did not prepare you for this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-practical-takeaways&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#my-practical-takeaways&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
My practical takeaways&lt;&#x2F;h2&gt;
&lt;p&gt;If I were to start over (and I wish someone had told me this):&lt;&#x2F;p&gt;
&lt;p&gt;Say yes to everything, especially the shitty projects. Doesn&#x27;t matter how boring or unglamorous, you will learn. Even if the project is a disaster you&#x27;ll learn what NOT to do, which is just as valuable.&lt;&#x2F;p&gt;
&lt;p&gt;Stop over-indexing on reading and courses. Build things, ship things, break things in production (in a non-catastrophic way, ideally). The suffering is part of the process.&lt;&#x2F;p&gt;
&lt;p&gt;Learn with a purpose, I mean use knowledge as a tool toward a goal. That goal doesn&#x27;t have to be concrete like &quot;build an app&quot;, it can be softer, like &quot;be a better engineer&quot;. Want to understand low-level memory management? Don&#x27;t watch 20 videos. Build a toy project in C or Zig, struggle with it, debug segfaults for hours. You&#x27;ll be a better engineer in your high-level abstracted day job because you&#x27;ll understand what&#x27;s happening underneath.&lt;&#x2F;p&gt;
&lt;p&gt;And for everyone:&lt;&#x2F;p&gt;
&lt;p&gt;Be patient. There&#x27;s no cheat code for experience, you can&#x27;t speedrun it and you can&#x27;t fake it (for more than a few interviews). Time and struggle are part of the equation.&lt;&#x2F;p&gt;
&lt;p&gt;Be humble about what you haven&#x27;t done. Reading about scaling Kubernetes pods is not the same as maintaining production systems at scale. I used to confuse the two.&lt;&#x2F;p&gt;
&lt;p&gt;You also need the right environment. You can&#x27;t get experience with high-scale systems if you don&#x27;t work somewhere with those problems, which isn&#x27;t your fault, but be aware of it and don&#x27;t claim experience you don&#x27;t have. And take on the weird, complicated projects at your day job, the messy ones nobody wants, the legacy systems, the fire drills. That&#x27;s where you learn the most.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;looking-back&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#looking-back&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Looking back&lt;&#x2F;h2&gt;
&lt;p&gt;I think about my younger (not so bright) self often. Why didn&#x27;t people take me seriously when I felt like a young genius?&lt;&#x2F;p&gt;
&lt;p&gt;Because I was (not really, but that&#x27;s what I thought) knowledge-rich and experience-poor. I could talk about systems I&#x27;d never built and explain patterns I&#x27;d never used in production. I had consumed information but hadn&#x27;t earned any scars. Shallow knowledge.&lt;&#x2F;p&gt;
&lt;p&gt;Ten years later I don&#x27;t consider myself as knowledgeable as I falsely thought I was at the start of my career, but I&#x27;m infinitely more capable. I&#x27;ve built things at scale. I&#x27;ve been paged just to find myself staring at the screen like it was abstract art. I&#x27;ve made mistakes that cost me years of life (hide the pain). I&#x27;ve shipped code (and by code I mean bugs) that millions of people used.&lt;&#x2F;p&gt;
&lt;p&gt;You can&#x27;t get any of that from reading. Now stop reading this and go build things, you twat.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>A learning framework to fight AI brain rot</title>
        <published>2025-11-22T00:00:00+00:00</published>
        <updated>2025-11-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/05-learning-framework/"/>
        <id>https://buenomini.com/posts/05-learning-framework/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/05-learning-framework/">&lt;p&gt;I want to be a better engineer, the kind who understands what&#x27;s happening underneath the code they ship. Someone like &lt;a href=&quot;https:&#x2F;&#x2F;thedailywtf.com&#x2F;articles&#x2F;the-inner-json-effect&quot;&gt;Tom&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This kind of comes from my own scare. A few months ago I realized I didn&#x27;t understand almost anything in my current project, even though I had onboarded without AI like a year ago. But around then my company made a deal with an AI company and we were allowed to use as much AI for coding as we wanted (not only allowed, actually encouraged), and I got to the point where I asked AI to do config change PRs like &quot;please enable this feature in QA&quot;. A literal one-liner.&lt;&#x2F;p&gt;
&lt;p&gt;When I realized I &lt;em&gt;could not&lt;&#x2F;em&gt; code on my own, I got scared. And this is the result of that scare.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ai-is-amazing-but-it-s-rotting-our-brains&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#ai-is-amazing-but-it-s-rotting-our-brains&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
AI is amazing, but it&#x27;s rotting our brains&lt;&#x2F;h2&gt;
&lt;p&gt;AI is incredible for learning, when used properly. The problem is that &quot;properly&quot; is a narrow and fuzzy window. Cross the line and you&#x27;re not learning anymore, you&#x27;re just putting together code you don&#x27;t understand. Stack Overflow copy-paste on steroids, tailored to your every need (at least with Stack Overflow you had to think about what to Google and read a bit, AI pushes the code at you and you don&#x27;t even have to read it).&lt;&#x2F;p&gt;
&lt;p&gt;What AI should be, IMO:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A knowledge base you can query&lt;&#x2F;li&gt;
&lt;li&gt;A fact-checker for your understanding (&quot;Is this how Rust&#x27;s borrow checker works?&quot;), and not the other way around&lt;&#x2F;li&gt;
&lt;li&gt;A rubber duck for reasoning through problems (maybe, I&#x27;m split on this one)&lt;&#x2F;li&gt;
&lt;li&gt;A documentation pointer (&quot;Where do I find info about Zig&#x27;s allocators?&quot;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;What AI should NOT do:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Write code&lt;&#x2F;li&gt;
&lt;li&gt;Debug your application. It&#x27;s yours, debug it yourself&lt;&#x2F;li&gt;
&lt;li&gt;Explain documentation too much. Reading documentation is one of the most valuable skills an engineer has, maybe second only to reading code&lt;&#x2F;li&gt;
&lt;li&gt;Save you from suffering and struggling when something isn&#x27;t working&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Maybe controversial, but the suffering is kind of the point. When you&#x27;re stuck on an error for an hour, reading docs and trying different approaches, that&#x27;s when the learning happens, and believe me, after that you won&#x27;t forget what the issue was. AI that &quot;fixes it for you&quot; takes that away.&lt;&#x2F;p&gt;
&lt;p&gt;At some point I wanted to stop using AI for learning entirely, but that isn&#x27;t great either. It&#x27;s a tool, and the best way I&#x27;ve found so far to use this hammer without every problem becoming a nail is to use AI as read-only. It doesn&#x27;t write my code, debug my programs or solve my problems, it&#x27;s just a fact-checker&#x2F;knowledge-base machine. It can&#x27;t access my code and it can&#x27;t tell me what to do.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;finding-the-right-project&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#finding-the-right-project&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Finding the right project&lt;&#x2F;h2&gt;
&lt;p&gt;When you&#x27;re learning something new, the project you pick matters more than you&#x27;d think.&lt;&#x2F;p&gt;
&lt;p&gt;Don&#x27;t try to invent something. You (probably) won&#x27;t. And even if you do, you&#x27;ll spend more time on product features than on learning, weeks on UI, auth and webhooks, and you never get to the deep programming concepts you wanted to learn. Who cares about the UI if your goal is to learn how something works under the hood. I get wanting to feel like you&#x27;re making something someone else could use, to justify the effort, but if you want to learn that&#x27;s the wrong goal. Learning is the goal. People using your app isn&#x27;t (and they probably won&#x27;t).&lt;&#x2F;p&gt;
&lt;p&gt;Don&#x27;t try to fill a gap either, that leads to massive feature creep. &quot;Oh, but this other tool doesn&#x27;t have X&quot;... and now you&#x27;re building features instead of learning fundamentals.&lt;&#x2F;p&gt;
&lt;p&gt;Rebuild something that already exists. It sounds counterintuitive, but build a text editor or a terminal emulator. A key-value store. Redis already exists, so does Vim, so does every terminal. But &lt;em&gt;you&lt;&#x2F;em&gt; haven&#x27;t built them. Whatever you make will be worse than what exists, and that&#x27;s fine, it actually takes away a lot of the distractions of building something, like deciding what it should do. If you&#x27;re writing your own terminal, you know &lt;em&gt;exactly&lt;&#x2F;em&gt; what it should do.&lt;&#x2F;p&gt;
&lt;p&gt;For one of my projects I picked an encrypted in-memory key-value store (in both Rust and Zig, more on that later). I get to learn:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Memory management (how do hash maps work?)&lt;&#x2F;li&gt;
&lt;li&gt;Network and I&#x2F;O multiplexing (epoll&#x2F;kqueue, event loops)&lt;&#x2F;li&gt;
&lt;li&gt;Wire protocols (how clients and servers talk and serialize the data)&lt;&#x2F;li&gt;
&lt;li&gt;Encryption (how do you encrypt data at rest? Can I have a fully encrypted data store without client-side encryption and an obfuscated KV store?)&lt;&#x2F;li&gt;
&lt;li&gt;Systems programming concepts without the feature noise&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The scope is clear: GET, SET, DELETE, EXISTS. Four commands. No lists, no pub&#x2F;sub.&lt;&#x2F;p&gt;
&lt;p&gt;If you&#x27;re stuck for ideas, check out &lt;a href=&quot;https:&#x2F;&#x2F;codecrafters.io&quot;&gt;CodeCrafters&lt;&#x2F;a&gt; (I haven&#x27;t used their service, but the project ideas are solid).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-value-of-exploration&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-value-of-exploration&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The value of exploration&lt;&#x2F;h2&gt;
&lt;p&gt;Learning a new language &quot;just because&quot; is underrated. You don&#x27;t have to be &lt;em&gt;that guy&lt;&#x2F;em&gt; who wants to start every project in the most obscure, new, non-adopted, beta language that doesn&#x27;t fit the use case at all. You might never use Zig professionally, or even again after this project, but you&#x27;ll still take something with you.&lt;&#x2F;p&gt;
&lt;p&gt;Different languages encode different philosophies:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Zig says: &quot;Keep it simple, give the programmer full control, trust them to manage memory correctly (which you probably won&#x27;t)&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Rust says: &quot;Fuck you, you know nothing. I&#x27;m better than you (which is also probably true)&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Javascript says: &quot;i NeED a sUePErSet tO Be KIndA usEFul&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;They&#x27;re different approaches to the same problem, and learning them makes your brain more malleable. You stop thinking in one paradigm and start seeing tradeoffs, so when you hit a problem you have more tools to reach for: &quot;oh, this is like Zig&#x27;s defer pattern&quot; or &quot;this would be cleaner with Rust&#x27;s Result type&quot;. Even if you never touch these languages again you carry that forward. You understand why memory safety matters, you recognize the cost of abstraction (and its power when it&#x27;s done right), and when manual control is worth the hassle and the risk.&lt;&#x2F;p&gt;
&lt;p&gt;I think exploration is one of the ways to form somewhat educated opinions about different ways to solve problems.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;learning-in-parallel&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#learning-in-parallel&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Learning in parallel&lt;&#x2F;h2&gt;
&lt;p&gt;There&#x27;s a problem with language comparisons. Ideally you build something in language A, then rebuild it in language B, but the second time you already know the solution, so you implement it better, faster and with fewer mistakes. Unfair comparison.&lt;&#x2F;p&gt;
&lt;p&gt;I wanted to compare Zig and Rust fairly, so I&#x27;m building the same encrypted key-value store in both languages, module by module, in parallel. First I built my own hash map implementation in Zig, then in Rust. After both were done I moved to the wire protocol (serialization and deserialization), first in Rust and then in Zig. Right now I&#x27;m powering through my own event loop for I&#x2F;O multiplexing in Zig, which will probably be orders of magnitude worse than any library, but again, that&#x27;s not the point.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s painful, there&#x27;s a &lt;strong&gt;lot&lt;&#x2F;strong&gt; of context switching and you&#x27;re maybe pushing too much information into your brain. Slow down. Don&#x27;t try to finish the project quickly, do the thing you actually set out to do, which is a fair 1:1 comparison.&lt;&#x2F;p&gt;
&lt;p&gt;What I&#x27;ve learned so far (2 modules in):&lt;&#x2F;p&gt;
&lt;p&gt;Module 1 in Zig: the language got out of my way, but I spent hours learning about memory. How does Zig&#x27;s HashMap work internally? (It stores pointers instead of copying the data.) What is a use-after-free vulnerability, how do you test for memory leaks.&lt;&#x2F;p&gt;
&lt;p&gt;Module 1 in Rust was the opposite. I didn&#x27;t think about memory management at all and focused on the type system instead. String vs &amp;amp;str, why do I need .clone() everywhere?&lt;&#x2F;p&gt;
&lt;p&gt;Module 2 in Zig: I found a subtle memory leak in my serializer. Some responses returned string literals (&quot;OK&quot;), others used allocPrint(), which allocates, and the caller didn&#x27;t know which responses needed freeing. In Zig you discover and document these patterns yourself.&lt;&#x2F;p&gt;
&lt;p&gt;Module 2 in Rust: enums with data are amazing. The type system made the parser clean and safe.&lt;&#x2F;p&gt;
&lt;p&gt;So both teach systems programming, just different parts of it. Zig makes you understand memory management deeply, Rust makes you understand ownership and borrowing deeply. Doing them in parallel keeps me from saying &quot;language A is better&quot; without understanding &lt;em&gt;why&lt;&#x2F;em&gt; each one makes the tradeoffs it does.&lt;&#x2F;p&gt;
&lt;p&gt;Before I started I was bullish on Zig and scared of Rust. So far I haven&#x27;t hit any complexity in Rust (no async stuff &lt;em&gt;yet&lt;&#x2F;em&gt;), and borrowing and ownership were actually pretty easy after everything I learned about memory while preparing for Zig. Right now I&#x27;m more bullish on Rust than Zig (I still find Zig amazing, and so far there&#x27;s space for both in my heart).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;project-logs-and-knowledge-bases&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#project-logs-and-knowledge-bases&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Project logs and knowledge bases&lt;&#x2F;h2&gt;
&lt;p&gt;This is the core of the framework, inspired by the &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Zettelkasten&quot;&gt;Zettelkasten&lt;&#x2F;a&gt; method (see &lt;a href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;How-Take-Smart-Notes-Nonfiction&#x2F;&quot;&gt;Sönke Ahrens &quot;How to Take Smart Notes&quot;&lt;&#x2F;a&gt; if you want the philosophy behind it). I keep two files for each project.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;knowledge-base.md&lt;&#x2F;code&gt; has concepts explained in my own words. Nothing copy-pasted from documentation, if I can&#x27;t explain it simply I don&#x27;t understand it.&lt;&#x2F;p&gt;
&lt;p&gt;Example entry:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;### Zig HashMap memory management
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;What HashMap allocates:
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - Internal structure: buckets, metadata, hash table arrays
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - Uses the allocator you pass at init()
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - map.deinit() frees only this internal structure
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;What HashMap does NOT allocate:
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - The actual key&#x2F;value data you pass to put()
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - It stores slices (pointer + length) pointing to YOUR memory
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - Does NOT copy the string data
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;Memory ownership problem:
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - HashMap stores pointers to wherever you allocated keys&#x2F;values
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - If that memory gets freed (e.g., arena deinit), hashmap has dangling pointers
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;  - Use-after-free = undefined behavior
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I would&#x27;ve missed this if I&#x27;d just asked AI &quot;how does HashMap work?&quot;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;project-log.md&lt;&#x2F;code&gt; is the raw record of the learning process.&lt;&#x2F;p&gt;
&lt;p&gt;Example entry:&lt;&#x2F;p&gt;
&lt;pre class=&quot;z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;### 2025-11-14 - Zig&amp;#39;s Real Difficulty: Understanding Memory, Not the Language
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;After implementing the Storage module, I realized Zig itself is not hard. Systems programming is hard.
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;The language syntax is simple and clean. But using it requires deeply understanding:
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;- How memory works (stack vs heap)
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;- What pointers actually are vs string literals
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;- How HashMap works internally (it doesn&amp;#39;t copy data, just stores pointers!)
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;- Manual lifetime tracking (all in your head)
&lt;&#x2F;span&gt;&lt;span class=&quot;z-text z-plain&quot;&gt;- Use-after-free, double-free, dangling pointer
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A bit more to convince you:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Writing is learning. Explaining a concept in your own words forces you to understand it.&lt;&#x2F;li&gt;
&lt;li&gt;Future reference. When I forget how HashMap works I don&#x27;t re-google, I read my own explanation.&lt;&#x2F;li&gt;
&lt;li&gt;Blog material. The raw logs become blog posts later, with the real struggles still in them.&lt;&#x2F;li&gt;
&lt;li&gt;Pattern recognition. Looking back I can see where I struggled and where things clicked.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The Zettelkasten method says notes should be written as if you&#x27;re teaching someone else, &quot;here&#x27;s how X works and why it matters&quot; instead of &quot;I learned X today&quot;, and that shift forces you to be clear. I highly recommend the book. It&#x27;s easy to read and it doesn&#x27;t spoon-feed you how to take your own notes, it just tells you how some prolific academics do it, and why, and it&#x27;s up to you what you take from it and what you ignore (unlike a lot of productivity and self-improvement books that sell you a magic recipe).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-i-am-now&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#where-i-am-now&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Where I am now&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m 2 modules into a 7-module project, and the framework is working. My knowledge base has grown organically, with entries on what a process actually is, IPC, memory management, wire protocols, encryption, concurrency models, different async strategies, and Zig&#x2F;Rust-specific patterns. I could already start writing blog posts about it, but I don&#x27;t have to, whenever I feel like it the notes will be there (the posts basically write themselves, you just pull a thread through the different notes).&lt;&#x2F;p&gt;
&lt;p&gt;The point is learning as much as possible per unit of time, finished project or not. So if you&#x27;re learning something new, try this:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Pick a project to rebuild (not invent)&lt;&#x2F;li&gt;
&lt;li&gt;Create knowledge-base.md and project-log.md&lt;&#x2F;li&gt;
&lt;li&gt;Write in your own words&lt;&#x2F;li&gt;
&lt;li&gt;Use AI as a read-only resource or not at all&lt;&#x2F;li&gt;
&lt;li&gt;Embrace the suffering&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Stack vs heap memory in Zig</title>
        <published>2025-11-16T00:00:00+00:00</published>
        <updated>2025-11-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/04-stack-vs-heap-zig/"/>
        <id>https://buenomini.com/posts/04-stack-vs-heap-zig/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/04-stack-vs-heap-zig/">&lt;p&gt;&lt;strong&gt;Note:&lt;&#x2F;strong&gt; This is based on Zig 0.15.2. Zig is pre-1.0 and constantly changing.&lt;&#x2F;p&gt;
&lt;p&gt;In Zig you have to choose where your data lives: stack or heap. Do it wrong and you get use-after-free bugs that might not even crash your app, they just silently corrupt your program and have you debugging ghosts forever. No biggie. (This isn&#x27;t specific to Zig btw, Zig actually makes it harder to mess up than C does, but it&#x27;s no Rust...)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stack-and-heap&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#stack-and-heap&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Stack and heap&lt;&#x2F;h2&gt;
&lt;p&gt;The stack is automatic. Declare a variable, it goes on the stack. Leave the scope, it&#x27;s gone.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-zig&quot;&gt;fn&lt;&#x2F;span&gt; &lt;span class=&quot;z-entity z-name z-function&quot;&gt;example&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-zig&quot;&gt;void&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-braces z-begin z-zig&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-member z-zig&quot;&gt;buffer&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; [&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;1024&lt;&#x2F;span&gt;]&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-zig&quot;&gt;undefined&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Stack allocated
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; ... use buffer ...
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-braces z-end z-zig&quot;&gt;}&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Automatically deallocated here
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It&#x27;s fast since it gets preallocated when the process starts, and if the OS can&#x27;t allocate it the process doesn&#x27;t start. Simple. That&#x27;s also why it&#x27;s limited in size.&lt;&#x2F;p&gt;
&lt;p&gt;The heap is manual. You allocate explicitly, you free explicitly.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-zig&quot;&gt;fn&lt;&#x2F;span&gt; &lt;span class=&quot;z-entity z-name z-function&quot;&gt;example&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter z-zig&quot;&gt;alloc&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-zig&quot;&gt;Allocator&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-zig&quot;&gt;!&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;void&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-braces z-begin z-zig&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; buffer &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; alloc.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;alloc&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;1024&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Heap allocated
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;defer&lt;&#x2F;span&gt; alloc.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;free&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;buffer&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Must explicitly free
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; ... use buffer ...
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-braces z-end z-zig&quot;&gt;}&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It&#x27;s slower, although it&#x27;s not like every allocation is a syscall (those happen per memory page). You allocate and deinit by hand, and the data can live beyond the scope of the function.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-use-after-free-bug&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-use-after-free-bug&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The use-after-free bug&lt;&#x2F;h2&gt;
&lt;p&gt;This is what bit me:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-zig&quot;&gt;fn&lt;&#x2F;span&gt; &lt;span class=&quot;z-entity z-name z-function&quot;&gt;bad&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; []&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-braces z-begin z-zig&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-member z-zig&quot;&gt;buffer&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; [&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;10&lt;&#x2F;span&gt;]&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-zig&quot;&gt;undefined&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Stack allocated
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; slice &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; buffer[0&lt;span class=&quot;z-keyword z-operator z-other z-zig&quot;&gt;..&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;5&lt;&#x2F;span&gt;]&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;return&lt;&#x2F;span&gt; slice&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; DANGER: slice points to stack memory
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-braces z-end z-zig&quot;&gt;}&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Stack deallocated, slice now points to garbage
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It compiles, it runs, it even works (until it doesn&#x27;t). The slice points to memory that&#x27;s been deallocated, so the caller gets a slice pointing at garbage. Classic use-after-free bug&#x2F;vulnerability.&lt;&#x2F;p&gt;
&lt;p&gt;The buffer has to outlive the slice:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-zig&quot;&gt;fn&lt;&#x2F;span&gt; &lt;span class=&quot;z-entity z-name z-function&quot;&gt;caller&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-zig&quot;&gt;void&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-braces z-begin z-zig&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-other z-member z-zig&quot;&gt;buffer&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; [&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;10&lt;&#x2F;span&gt;]&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-constant z-language z-zig&quot;&gt;undefined&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Buffer lives here
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; slice &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-function z-zig&quot;&gt;good&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-bitwise z-zig&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;buffer&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;      &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Slice points to buffer
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; ... use slice ...
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-braces z-end z-zig&quot;&gt;}&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; NOW buffer is deallocated (after we&amp;#39;re done)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-zig&quot;&gt;fn&lt;&#x2F;span&gt; &lt;span class=&quot;z-entity z-name z-function&quot;&gt;good&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter z-zig&quot;&gt;buffer&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;:&lt;&#x2F;span&gt; []&lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt; []&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-type z-zig&quot;&gt;u8&lt;&#x2F;span&gt; &lt;span class=&quot;z-punctuation z-section z-braces z-begin z-zig&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;    &lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;return&lt;&#x2F;span&gt; buffer[0&lt;span class=&quot;z-keyword z-operator z-other z-zig&quot;&gt;..&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-zig&quot;&gt;5&lt;&#x2F;span&gt;]&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Safe: buffer lives in caller
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-braces z-end z-zig&quot;&gt;}&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Data must live at least as long as any reference to it. This is why I think Zig is nice, it makes you pass allocators explicitly, which kinda flags &lt;a href=&quot;https:&#x2F;&#x2F;ziglang.org&#x2F;documentation&#x2F;master&#x2F;#Where-are-the-bytes&quot;&gt;where the bytes are&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;So roughly, stack for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Local variables, temporary data&lt;&#x2F;li&gt;
&lt;li&gt;Small, fixed-size buffers&lt;&#x2F;li&gt;
&lt;li&gt;Data that doesn&#x27;t outlive the function&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;(more on how this works for TCP requests in the TigerBeetle example below)&lt;&#x2F;p&gt;
&lt;p&gt;And heap for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Long-lived data (persists beyond function scope)&lt;&#x2F;li&gt;
&lt;li&gt;Dynamically sized data&lt;&#x2F;li&gt;
&lt;li&gt;Large allocations&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;rust-vs-zig-here&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#rust-vs-zig-here&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Rust vs Zig here&lt;&#x2F;h2&gt;
&lt;p&gt;Rust&#x27;s borrow checker catches this at compile time:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-rust&quot;&gt;fn&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function z-rust&quot;&gt;bad&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt; &lt;span class=&quot;z-meta z-function z-return-type z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-rust&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; s &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-type z-rust&quot;&gt;String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-path z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;from&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;hello&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-keyword z-operator z-bitwise z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;s  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Compiler error: s is dropped, can&amp;#39;t return reference
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-rust&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Rust enforces lifetimes, so you can&#x27;t return a reference to stack-allocated data. The compiler won&#x27;t let you.&lt;&#x2F;p&gt;
&lt;p&gt;Zig compiles that code. It trusts you to manage lifetimes correctly, meaning you track by hand where the data was allocated, how long it should live and who owns the deinit, and if you get it wrong you have runtime bugs or exploitable vulnerabilities. The language is simple and gives you a lot of tools to not shoot yourself in the foot, but it doesn&#x27;t take the gun away from you.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;no-heap-at-all&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#no-heap-at-all&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
No heap at all&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;tigerbeetle.com&#x2F;&quot;&gt;TigerBeetle&lt;&#x2F;a&gt;, a financial database written in Zig, takes this to the extreme: zero heap allocations at runtime.&lt;&#x2F;p&gt;
&lt;p&gt;Everything is stack-allocated because they know exactly how much memory they need upfront. With a single-threaded event loop and no concurrent operations to manage, plus very domain-specific message framing (128bits per message, 8k messages in a batch), they know beforehand that a payload is going to be around 1MB. When you can bound your memory usage at compile time like that, you can avoid the heap entirely, even when the data comes from user requests. No allocator overhead, no fragmentation, no use-after-free bugs from heap memory, and more performance (which matters a lot for a transactional database).&lt;&#x2F;p&gt;
&lt;p&gt;Most applications will need the heap tho. But it&#x27;s a fascinating proof that you can build serious systems with just the stack if you design for it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Zig hashmap gotcha: it doesn&#x27;t copy your data</title>
        <published>2025-11-15T00:00:00+00:00</published>
        <updated>2025-11-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/03-zig-hashmap-memory/"/>
        <id>https://buenomini.com/posts/03-zig-hashmap-memory/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/03-zig-hashmap-memory/">&lt;p&gt;&lt;strong&gt;Note:&lt;&#x2F;strong&gt; This is based on Zig 0.15.2. Zig is pre-1.0 and constantly changing.&lt;&#x2F;p&gt;
&lt;p&gt;This one bit me hard when I built a key-value store in Zig knowing little about manual memory management: HashMap doesn&#x27;t copy your data. It stores pointers to wherever you allocated it.&lt;&#x2F;p&gt;
&lt;p&gt;When you create a HashMap in Zig, it allocates its internal structures:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; map &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; std.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;StringHashMap&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;[]&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; u8&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;init&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;allocator&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;defer&lt;&#x2F;span&gt; map.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;deinit&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;allocator&lt;&#x2F;code&gt; is used for buckets, metadata and hash table arrays, and &lt;code&gt;map.deinit()&lt;&#x2F;code&gt; frees only this internal structure. That was a good gotcha. Because it takes an allocator I thought it would also allocate the values and free them when the map was gone, but it doesn&#x27;t allocate the key&#x2F;value data you pass to &lt;code&gt;put()&lt;&#x2F;code&gt; at all:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; key &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-zig&quot;&gt;&amp;quot;mykey&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; value &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-zig&quot;&gt;&amp;quot;myvalue&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; map.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;put&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;key&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; value&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; HashMap stores slices (pointer + length)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No copy of the string data. It stores slices pointing to YOUR memory, wherever you allocated those strings.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-memory-ownership-problem&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-memory-ownership-problem&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The memory ownership problem&lt;&#x2F;h2&gt;
&lt;p&gt;Which gives you a subtle bug like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Request-scoped arena allocator
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; arena &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; std.heap.ArenaAllocator.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;init&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;allocator&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;defer&lt;&#x2F;span&gt; arena.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;deinit&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Frees ALL arena memory at once
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Parse request, allocate keys&#x2F;values from arena
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; key &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; arena.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;allocator&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;dupe&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-zig&quot;&gt;&amp;quot;mykey&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; value &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; arena.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;allocator&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;dupe&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-zig&quot;&gt;&amp;quot;myvalue&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Store in HashMap
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; map.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;put&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;key&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; value&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Request ends, arena is freed
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; HashMap now has dangling pointers!
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When the arena is freed, the memory &lt;code&gt;key&lt;&#x2F;code&gt; and &lt;code&gt;value&lt;&#x2F;code&gt; pointed to is gone, but the HashMap still points at it. Use-after-free. And it doesn&#x27;t necessarily crash, it might work 99% of the time and then randomly fail. Or worse, it&#x27;s a big ass security bug.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;separate-allocators-for-separate-lifetimes&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#separate-allocators-for-separate-lifetimes&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Separate allocators for separate lifetimes&lt;&#x2F;h2&gt;
&lt;p&gt;In hindsight it&#x27;s obvious. Why would a hashmap that needs to persist between TCP requests use an allocator that gets deinitialized when the connection ends... Request-scoped memory (the arena for parsing) and storage-scoped memory (keys and values in the HashMap) have different lifetimes, so you handle them separately, and that means copying:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; 1. Parse request into arena-allocated buffer (temporary)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;var&lt;&#x2F;span&gt; arena &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; std.heap.ArenaAllocator.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;init&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;allocator&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;defer&lt;&#x2F;span&gt; arena.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;deinit&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; parsed_key &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-function z-zig&quot;&gt;parseKey&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;arena.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;allocator&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; input&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; 2. Allocate permanent storage from GPA
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; stored_key &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; gpa.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;dupe&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; parsed_key&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; 3. Store GPA-allocated data in HashMap
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; map.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;put&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;stored_key&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; stored_value&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; 4. Arena deinit frees parse buffer (safe - data is copied)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; 5. GPA-allocated key&#x2F;value stays alive in HashMap
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One extra allocation + memcpy per &lt;code&gt;PUT&lt;&#x2F;code&gt;. It&#x27;s necessary because request buffers and storage have different lifetimes. Redis does this too (blessed pattern then :P), it copies data from read buffers into storage allocations.&lt;&#x2F;p&gt;
&lt;p&gt;You could also skip the arena and allocate everything from the GPA:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zig&quot; class=&quot;language-zig z-code&quot;&gt;&lt;code class=&quot;language-zig&quot; data-lang=&quot;zig&quot;&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Parse and store both use GPA
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-storage z-modifier z-zig&quot;&gt;const&lt;&#x2F;span&gt; key &lt;span class=&quot;z-keyword z-operator z-assignment z-zig&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; gpa.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;dupe&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;u8&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-zig&quot;&gt;&amp;quot;mykey&amp;quot;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-conditional z-zig&quot;&gt;try&lt;&#x2F;span&gt; map.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;put&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;key&lt;span class=&quot;z-punctuation z-separator z-zig&quot;&gt;,&lt;&#x2F;span&gt; value&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-comment z-line z-zig&quot;&gt;&#x2F;&#x2F; Manually free temporary parse buffers
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-zig&quot;&gt;&lt;span class=&quot;z-keyword z-control z-zig&quot;&gt;defer&lt;&#x2F;span&gt; gpa.&lt;span class=&quot;z-variable z-function z-zig&quot;&gt;free&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-parens z-begin z-zig&quot;&gt;(&lt;&#x2F;span&gt;temp_buffer&lt;span class=&quot;z-punctuation z-section z-parens z-end z-zig&quot;&gt;)&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-zig&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That avoids the copy, but then you have to track by hand what&#x27;s temporary and what&#x27;s permanent. Which I don&#x27;t want to do. What is this, C?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-rust-doesn-t-have-this-problem&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#why-rust-doesn-t-have-this-problem&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Why Rust doesn&#x27;t have this problem&lt;&#x2F;h2&gt;
&lt;p&gt;In Rust the borrow checker prevents this at compile time:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-rust&quot;&gt;fn&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function z-rust&quot;&gt;bad&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-rust&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-modifier z-rust&quot;&gt;mut&lt;&#x2F;span&gt; map &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-meta z-path z-rust&quot;&gt;HashMap&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;new&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-rust&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;        &lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; key &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-type z-rust&quot;&gt;String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-path z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;from&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;mykey&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;        map&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function z-rust&quot;&gt;insert&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;key&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;,&lt;&#x2F;span&gt; value&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; key is moved into HashMap
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-rust&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; key can&amp;#39;t be dropped - HashMap owns it now
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-rust&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Rust&#x27;s ownership rules make whoever owns the data responsible for freeing it, so you can&#x27;t accidentally create dangling pointers. At the same time, you mostly don&#x27;t allocate by hand in Rust as far as I know, the language abstracts it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>String vs &amp;str in Rust</title>
        <published>2025-11-14T00:00:00+00:00</published>
        <updated>2025-11-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/02-string-vs-str-rust/"/>
        <id>https://buenomini.com/posts/02-string-vs-str-rust/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/02-string-vs-str-rust/">&lt;p&gt;Rust has two main string types, and when you&#x27;re coming from languages with one string type it&#x27;s confusing as hell. This is what I learned building an in-memory key-value store.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;string-the-owned-type&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#string-the-owned-type&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
String, the owned type&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;String&lt;&#x2F;code&gt; is heap-allocated and growable. It owns the underlying data and is responsible for cleaning it up when it goes out of scope.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; &lt;span class=&quot;z-storage z-modifier z-rust&quot;&gt;mut&lt;&#x2F;span&gt; s &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-type z-rust&quot;&gt;String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-path z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;from&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;hello&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;s&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function z-rust&quot;&gt;push_str&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt; world&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Can modify, it owns the data
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; s is automatically freed when it goes out of scope
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You use &lt;code&gt;String&lt;&#x2F;code&gt; when you create stuff at runtime (user input, a request, etc.), or you need to mutate it, or it has to live beyond the current function.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;str-the-borrowed-type&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#str-the-borrowed-type&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
&amp;amp;str, the borrowed type&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt; is a string slice, just a view into string data. It&#x27;s borrowed because it doesn&#x27;t own the data, it&#x27;s only a pointer to the start plus the length.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; s &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-type z-rust&quot;&gt;String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-path z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;from&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;hello world&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; slice&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;str&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;s&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-rust&quot;&gt;0&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-range z-rust&quot;&gt;..&lt;&#x2F;span&gt;&lt;span class=&quot;z-constant z-numeric z-integer z-decimal z-rust&quot;&gt;5&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; &amp;quot;hello&amp;quot;, just a view
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Since it&#x27;s borrowed, the compiler won&#x27;t let you use it after the owner goes out of scope:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-rust&quot;&gt;fn&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function z-rust&quot;&gt;bad&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt; &lt;span class=&quot;z-meta z-function z-return-type z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-rust&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; s &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-type z-rust&quot;&gt;String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-path z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;from&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;hello&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-keyword z-operator z-bitwise z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;s  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Compiler error: s is dropped, can&amp;#39;t return reference
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-rust&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt; is for when you&#x27;re reading, analyzing or parsing a string without modifying it, and for function parameters.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-i-cared&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#why-i-cared&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Why I cared&lt;&#x2F;h2&gt;
&lt;p&gt;In my key-value store I parse commands from TCP requests. The request buffer is temporary (arena-allocated), but stored values have to live permanently.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Parse from temporary request buffer (&amp;amp;str)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-rust&quot;&gt;fn&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function z-rust&quot;&gt;parse_command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-variable z-parameter z-rust&quot;&gt;input&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt; &lt;span class=&quot;z-meta z-function z-return-type z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt; Command&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-rust&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; input is borrowed, lives in request scope
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-rust&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Store needs owned data (String)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-function z-rust&quot;&gt;fn&lt;&#x2F;span&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-entity z-name z-function z-rust&quot;&gt;set&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-keyword z-operator z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-modifier z-rust&quot;&gt;mut&lt;&#x2F;span&gt; &lt;span class=&quot;z-variable z-parameter z-rust&quot;&gt;self&lt;&#x2F;span&gt;, &lt;span class=&quot;z-variable z-parameter z-rust&quot;&gt;key&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;:&lt;&#x2F;span&gt; String, &lt;span class=&quot;z-variable z-parameter z-rust&quot;&gt;value&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;:&lt;&#x2F;span&gt; String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-parameters z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-parameters z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt; &lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-begin z-rust&quot;&gt;{&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;    &lt;span class=&quot;z-variable z-language z-rust&quot;&gt;self&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;map&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function z-rust&quot;&gt;insert&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;key&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;,&lt;&#x2F;span&gt; value&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; Needs owned data
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-meta z-function z-rust&quot;&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-block z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-block z-end z-rust&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If I tried to store &lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt; references into the request buffer I&#x27;d have dangling pointers as soon as the request ends, and the compiler catches that at compile time.&lt;&#x2F;p&gt;
&lt;p&gt;Size-wise, &lt;code&gt;String&lt;&#x2F;code&gt; is 24 bytes on 64-bit systems:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Pointer to heap data (8 bytes)&lt;&#x2F;li&gt;
&lt;li&gt;Length (8 bytes)&lt;&#x2F;li&gt;
&lt;li&gt;Capacity (8 bytes)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt; is 16 bytes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Pointer to data (8 bytes)&lt;&#x2F;li&gt;
&lt;li&gt;Length (8 bytes)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;No capacity because it can&#x27;t grow, it&#x27;s read-only.&lt;&#x2F;p&gt;
&lt;p&gt;Converting between them:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust z-code&quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; owned &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-support z-type z-rust&quot;&gt;String&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-path z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-accessor z-rust&quot;&gt;::&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;from&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;hello&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; borrowed&lt;span class=&quot;z-punctuation z-separator z-rust&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;str&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-keyword z-operator z-bitwise z-rust&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;owned&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; String -&amp;gt; &amp;amp;str (cheap)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; borrowed &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; &lt;span class=&quot;z-string z-quoted z-double z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-begin z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;hello&lt;span class=&quot;z-punctuation z-definition z-string z-end z-rust&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; &amp;amp;str (string literal)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-source z-rust&quot;&gt;&lt;span class=&quot;z-storage z-type z-rust&quot;&gt;let&lt;&#x2F;span&gt; owned &lt;span class=&quot;z-keyword z-operator z-assignment z-rust&quot;&gt;=&lt;&#x2F;span&gt; borrowed&lt;span class=&quot;z-punctuation z-accessor z-dot z-rust&quot;&gt;.&lt;&#x2F;span&gt;&lt;span class=&quot;z-support z-function z-rust&quot;&gt;to_string&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-begin z-rust&quot;&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-meta z-group z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-section z-group z-end z-rust&quot;&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;span class=&quot;z-punctuation z-terminator z-rust&quot;&gt;;&lt;&#x2F;span&gt;  &lt;span class=&quot;z-comment z-line z-double-slash z-rust&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-rust&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt; &amp;amp;str -&amp;gt; String (allocates)
&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;String literals like &lt;code&gt;&quot;hello&quot;&lt;&#x2F;code&gt; are &lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt; by default, they point to data baked into the binary at compile time.&lt;&#x2F;p&gt;
&lt;p&gt;The distinction felt annoying at first, but it forces you to think about who owns the memory. After dealing with use-after-free bugs in Zig (where you track all of this in your head), having the compiler enforce it is kind of... nice.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rebuilding P2P secret sharing</title>
        <published>2025-11-11T00:00:00+00:00</published>
        <updated>2025-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Julian Bonomini
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://buenomini.com/posts/01-hush/"/>
        <id>https://buenomini.com/posts/01-hush/</id>
        
        <content type="html" xml:base="https://buenomini.com/posts/01-hush/">&lt;p&gt;I just spent last weekend building &lt;em&gt;hush&lt;&#x2F;em&gt;, a peer-to-peer, end-to-end encrypted secret sharing tool. &lt;a href=&quot;https:&#x2F;&#x2F;magic-wormhole.readthedocs.io&#x2F;en&#x2F;latest&#x2F;&quot;&gt;Magic Wormhole&lt;&#x2F;a&gt; already exists. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;schollz&#x2F;croc&quot;&gt;Croc&lt;&#x2F;a&gt; exists. So what gives?&lt;&#x2F;p&gt;
&lt;p&gt;I wanted to learn, and I also wanted a CLI we could actually use at my current job to share secrets across offices (we have a split-tunnel VPN).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-hush-does&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#what-hush-does&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
What hush does&lt;&#x2F;h2&gt;
&lt;p&gt;Very basic stuff:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Share secrets: send text to a peer with a code like &quot;7-Ld89GsHO-gGHps31&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Transfer files: send files up to 10MB, end-to-end encrypted&lt;&#x2F;li&gt;
&lt;li&gt;Encrypted chat: real-time P2P chat with no server in the middle&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;All of it works across local networks and VPNs, with zero trust in any relay server (because there isn&#x27;t one). Didn&#x27;t want to deal with the corporate process to spin up a STUN server.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-build-what-already-exists&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#why-build-what-already-exists&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Why build what already exists?&lt;&#x2F;h2&gt;
&lt;p&gt;Early in the project I talked about learning strategies with a coworker, focused learning vs exploratory learning. For this one I wanted to go broad and cover areas where I had absolutely no idea how things actually worked. Of course I knew how to use a lot of it, but knowing how to use something and knowing how it works are very different things, and networking, cryptography and security were always a gray box for me.&lt;&#x2F;p&gt;
&lt;p&gt;The best way for me to learn that is to build something I&#x27;d use myself, in this case to share secrets with coworkers across our VPN.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-basic-networking-knowledge&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-basic-networking-knowledge&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The basic networking knowledge&lt;&#x2F;h2&gt;
&lt;p&gt;I knew how to use TCP, UDP and P2P. I didn&#x27;t &lt;em&gt;understand&lt;&#x2F;em&gt; them until I tried to build with them.&lt;&#x2F;p&gt;
&lt;p&gt;UDP for discovery, TCP for data is pretty straightforward but worth a quick mention. To find peers on a network you broadcast a UDP message to everyone who&#x27;s listening. It&#x27;s like shouting &quot;YOOO, anyone there?&quot; into a room, and UDP doesn&#x27;t care if anyone gets the message. The good old fire and forget. Once you&#x27;ve found someone you could keep talking over UDP, but you probably want TCP, which is reliable, ordered, and the other side confirms it got your message with an ACK.&lt;&#x2F;p&gt;
&lt;p&gt;TCP being a byte stream is the one I didn&#x27;t know... I thought I could just &lt;code&gt;write()&lt;&#x2F;code&gt; some data on one end and &lt;code&gt;read()&lt;&#x2F;code&gt; it on the other. Nope. TCP gives you a continuous stream of bytes. One &lt;code&gt;read()&lt;&#x2F;code&gt; might give you half of what you sent, or data from two different &lt;code&gt;write()&lt;&#x2F;code&gt; calls smooshed together.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s why &lt;a href=&quot;https:&#x2F;&#x2F;buenomini.com&#x2F;posts&#x2F;09-wire-protocol-framing&#x2F;&quot;&gt;message framing&lt;&#x2F;a&gt; exists. You prefix every message with its size (4 bytes for a uint32), then read exactly that many bytes. Simple, but not obvious until you hit the problem.&lt;&#x2F;p&gt;
&lt;p&gt;Then the VPN. Every VPN can be configured differently, and ours is split-tunnel (only some of the traffic gets routed through it). To figure out what was allowed, I just did a few pings to the internal IP of a coworker in a different office. Which worked! Yay! TCP was viable and UDP was not, meaning I can&#x27;t auto-discover anyone outside my local network. There are workarounds, like a STUN&#x2F;TURN server, but I didn&#x27;t want a server. So I settled for an &lt;code&gt;--ip&lt;&#x2F;code&gt; flag that skips discovery and connects directly over TCP to an internal VPN IP. Not as elegant as automatic discovery, but it works. The IP can go over the same out-of-band channel as the code, and the CLI copies it to the clipboard for convenience.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-cryptography-learning&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#the-cryptography-learning&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
The cryptography learning&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m not a cryptographer. I knew I shouldn&#x27;t (and couldn&#x27;t) roll my own crypto, but I wanted to understand it a bit better.&lt;&#x2F;p&gt;
&lt;p&gt;PAKE (password-authenticated key exchange) was the biggest lesson of the project, and it&#x27;s not common knowledge like TCP&#x2F;UDP. I didn&#x27;t know how two computers could agree on a shared encryption key over an insecure network. You could use a normal shared password, but that didn&#x27;t feel safe at all. Encrypting with the password directly is too weak, an attacker who intercepts the encrypted data can brute-force millions of passwords offline until one works.&lt;&#x2F;p&gt;
&lt;p&gt;PAKE protocols (like SPAKE2, which hush uses) solve this:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Both parties start with the weak password (the hush code, like &quot;7-Ld89GsHO-gGHps31&quot;)&lt;&#x2F;li&gt;
&lt;li&gt;The password is shared out-of-band, meaning through a different channel than the encrypted data (Slack, Signal, in person, etc.)&lt;&#x2F;li&gt;
&lt;li&gt;They exchange messages over the network (an attacker seeing them is no problem!)&lt;&#x2F;li&gt;
&lt;li&gt;Through some clever math, both sides independently derive the &lt;em&gt;same&lt;&#x2F;em&gt; strong encryption key&lt;&#x2F;li&gt;
&lt;li&gt;The password is never sent over the network&lt;&#x2F;li&gt;
&lt;li&gt;An attacker can&#x27;t brute-force offline. They&#x27;d have to try each guess against the live protocol&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;So an attacker would need to compromise &lt;em&gt;both&lt;&#x2F;em&gt; the channel where you share the code (like Slack) &lt;em&gt;and&lt;&#x2F;em&gt; the network where the encrypted data flows. And the code is one-time use, so even if both channels are compromised the attacker also has to be quick enough to intercept it.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the flow in hush (thanks AI for the nice mermaid art):&lt;&#x2F;p&gt;

&lt;figure class=&quot;inline mermaid-fig&quot;&gt;&lt;pre class=&quot;mermaid&quot;&gt;sequenceDiagram
    participant Sender
    participant Network
    participant Receiver

    Note over Sender,Receiver: Both know password &amp;quot;7-Ld89GsHO-gGHps31&amp;quot;

    Sender-&amp;gt;&amp;gt;Sender: Generate PAKE initial message
    Sender-&amp;gt;&amp;gt;Network: Send initial message
    Network-&amp;gt;&amp;gt;Receiver: Forward message

    Receiver-&amp;gt;&amp;gt;Receiver: Process message with Update()
    Receiver-&amp;gt;&amp;gt;Receiver: Generate response with Bytes()
    Receiver-&amp;gt;&amp;gt;Network: Send response
    Network-&amp;gt;&amp;gt;Sender: Forward response

    Sender-&amp;gt;&amp;gt;Sender: Process response with Update()

    Note over Sender,Receiver: Both now have same strong key!

    Sender-&amp;gt;&amp;gt;Network: Send encrypted secret
    Network-&amp;gt;&amp;gt;Receiver: Forward encrypted secret
    Receiver-&amp;gt;&amp;gt;Receiver: Decrypt with derived key&lt;&#x2F;pre&gt;&lt;&#x2F;figure&gt;
&lt;script type=&quot;module&quot;&gt;
  import mermaid from &quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;npm&#x2F;mermaid@11&#x2F;dist&#x2F;mermaid.esm.min.mjs&quot;;
  const dark = document.documentElement.classList.contains(&quot;dark&quot;);
  mermaid.initialize({ startOnLoad: true, theme: dark ? &quot;dark&quot; : &quot;neutral&quot;, fontFamily: &quot;Mona Sans, system-ui, sans-serif&quot; });
&lt;&#x2F;script&gt;
&lt;p&gt;Even if someone intercepts every message on the network, they can&#x27;t derive the key without the original password, and they can&#x27;t brute-force it offline either. They&#x27;d have to take part in the protocol for every single guess, which makes it annoying.&lt;&#x2F;p&gt;
&lt;p&gt;I used the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;schollz&#x2F;pake&quot;&gt;schollz&#x2F;pake&lt;&#x2F;a&gt; library (a SPAKE2 implementation) instead of implementing it myself. Because of course...&lt;&#x2F;p&gt;
&lt;h2 id=&quot;security-from-the-start&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#security-from-the-start&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Security from the start&lt;&#x2F;h2&gt;
&lt;p&gt;Building with security in mind from the start was very interesting, and definitely not something I&#x27;m used to.&lt;&#x2F;p&gt;
&lt;p&gt;One of hush&#x27;s features is sending files up to 10MB. When I first implemented receiving, I saved files with their original filename. Very reasonable for my non security oriented brain.&lt;&#x2F;p&gt;
&lt;p&gt;Well, no... An attacker could send a file named &lt;code&gt;..&#x2F;..&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt; and overwrite critical system files. That&#x27;s called a path traversal attack. And even if you sanitize the path, the filename itself can be dangerous, it can overwrite a file in your current directory if you run hush from a sensitive folder. I didn&#x27;t want an annoying interactive CLI where you inspect the name and path, so I just ignore the original filename entirely. hush generates a safe name from a timestamp and the file extension, something like &lt;code&gt;hush_1699747123.pdf&lt;&#x2F;code&gt;, and renaming it is up to the user.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m also not a trusting person, so I wouldn&#x27;t trust a random tool with my secrets. That&#x27;s why there are no relay servers and no logs, and the keys are ephemeral and disappear after the transfer. Even if someone compromises the server (that doesn&#x27;t exist), they can&#x27;t get to your secrets. I checked every design decision against this. You don&#x27;t need to trust anything because there&#x27;s nothing to trust.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-quickly-go-clicked&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#how-quickly-go-clicked&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
How quickly Go clicked&lt;&#x2F;h2&gt;
&lt;p&gt;This was my first &quot;real&quot; Go project, and I was shocked at how fast I got proficient. After about half a day of reading docs and writing code I was shipping features. Go&#x27;s simplicity is damn awesome.&lt;&#x2F;p&gt;
&lt;p&gt;I do wonder if that simplicity runs into trouble once genuinely hard problems need solving tho. One can argue those don&#x27;t exist, and that if you&#x27;re a good engineer anything can be made simple. Anyone who argues that has never hit a genuinely hard problem.&lt;&#x2F;p&gt;
&lt;p&gt;Explicit error handling is great. Maybe not Rust great, and a bit verbose, but great. Combined with assertions it makes a good safety net.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;takeaway&quot;&gt;&lt;a class=&quot;hn&quot; href=&quot;#takeaway&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;&#x2F;a&gt;
Takeaway&lt;&#x2F;h2&gt;
&lt;p&gt;I didn&#x27;t need to build this. Magic Wormhole exists and is probably better, and Croc is literally from the same person as the PAKE library I used, also in Go, with more and better features than hush.&lt;&#x2F;p&gt;
&lt;p&gt;But I learned way more building hush than I would have reading docs or watching videos (or chatting with everyone&#x27;s beloved AI). You don&#x27;t need a new idea to build something. I&#x27;d even argue the opposite, that the best projects are often the ones that help you understand old solutions, and that&#x27;s going to be my next post!&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
