<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Valentyn Kit</title>
    <description>The latest articles on DEV Community by Valentyn Kit (@valentynkit).</description>
    <link>https://dev.to/valentynkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1466426%2Fcbad2b27-499a-43e4-a45f-01899556d6a1.jpg</url>
      <title>DEV Community: Valentyn Kit</title>
      <link>https://dev.to/valentynkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/valentynkit"/>
    <language>en</language>
    <item>
      <title>Rust's compile errors are other languages' production bugs</title>
      <dc:creator>Valentyn Kit</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:51:35 +0000</pubDate>
      <link>https://dev.to/valentynkit/rusts-compile-errors-are-other-languages-production-bugs-32dm</link>
      <guid>https://dev.to/valentynkit/rusts-compile-errors-are-other-languages-production-bugs-32dm</guid>
      <description>&lt;p&gt;Rust refused to compile two lines of mine last week: a Vec of floats and a call to &lt;code&gt;.sort()&lt;/code&gt;. Run the equivalent in Python with one NaN in the list and &lt;code&gt;sorted()&lt;/code&gt; returns normally, having produced a list that is not sorted. Nothing crashes, nothing warns; the wrong answer comes back looking like a right one.&lt;/p&gt;

&lt;p&gt;I've been writing Rust daily for a few weeks now, rebuilding a C Redis clone in it and keeping notes on every fight I lose to the compiler. A pattern keeps repeating: I hit a refusal, decide the compiler is being difficult, dig into the why, and find a bug I have almost certainly shipped in another language.&lt;/p&gt;

&lt;p&gt;This piece walks through four of those refusals, with the receipts, then closes with the place Rust goes quiet instead. Everything below was run on rustc 1.95.0, Python 3.14.3, and Node 25.9.0 on 2026-07-08; the outputs are pasted, not paraphrased.&lt;/p&gt;

&lt;p&gt;The refusals come in grades, and I'll be honest about which is which: three are hard compile errors, the fourth is a warning you learn to stop scrolling past.&lt;/p&gt;

&lt;h2&gt;
  
  
  Floats have no total order
&lt;/h2&gt;

&lt;p&gt;The two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="nf"&gt;.sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[E0277]: the trait bound `f64: Ord` is not satisfied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort()&lt;/code&gt; requires &lt;code&gt;Ord&lt;/code&gt;, a total order: for any two values, exactly one of less-than, equal, greater-than holds. Sorting algorithms are built on that contract. A merge step trusts its comparisons; a partition trusts that every element lands on one side of the pivot. Integers can keep that promise. Floats carry one value that can't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;    &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;    &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust encodes "this comparison can fail" in the type: &lt;code&gt;partial_cmp&lt;/code&gt; returns an &lt;code&gt;Option&amp;lt;Ordering&amp;gt;&lt;/code&gt;, and for NaN it returns &lt;code&gt;None&lt;/code&gt;. So &lt;code&gt;f64&lt;/code&gt; gets &lt;code&gt;PartialOrd&lt;/code&gt; and not &lt;code&gt;Ord&lt;/code&gt;, and the sort refuses to build.&lt;/p&gt;

&lt;p&gt;Here is the bug that refusal prevents. Python, same values, three input orders (Python 3.14.3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first result looks perfect. The second has nan in the middle and 3.0 after it. The third came back in its original, unsorted order. &lt;code&gt;sorted()&lt;/code&gt; returned normally all three times.&lt;/p&gt;

&lt;p&gt;The mechanism is the broken contract: Python's sort trusts its comparisons, every comparison against nan answers false, and the damage depends on where nan happens to sit in the input. A test suite without a NaN in an unlucky position passes forever. Production data eventually finds the unlucky position.&lt;/p&gt;

&lt;p&gt;JavaScript behaves the same way once you hand &lt;code&gt;sort&lt;/code&gt; the numeric comparator everyone writes. &lt;code&gt;(x, y) =&amp;gt; x - y&lt;/code&gt; returns NaN for NaN pairs, which makes the comparator inconsistent, and ECMAScript makes the resulting order implementation-defined. On Node 25.9.0, &lt;code&gt;[3, 1, 2, NaN]&lt;/code&gt; comes out &lt;code&gt;[1, 2, 3, NaN]&lt;/code&gt; and &lt;code&gt;[NaN, 3, 1, 2]&lt;/code&gt; comes out &lt;code&gt;[NaN, 1, 2, 3]&lt;/code&gt;: NaN stays wherever it started.&lt;/p&gt;

&lt;p&gt;C is the bluntest of the three: &lt;code&gt;qsort&lt;/code&gt; with a comparator that doesn't impose a total order is undefined behavior outright (C11 7.22.5).&lt;/p&gt;

&lt;p&gt;The fix costs one comparator, and it's what the type system was asking for all along:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.total_cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;total_cmp&lt;/code&gt; is IEEE 754's totalOrder predicate, a genuine total order over every float bit pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NEG_INFINITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;INFINITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.total_cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// [NaN, -inf, -0.0, 0.0, 1.0, inf, NaN]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Negative NaN first, positive NaN last, negative zero before positive zero. You don't need to memorize the order; you need it to exist. If NaN in your data is itself a bug, &lt;code&gt;v.retain(|x| !x.is_nan())&lt;/code&gt; before sorting is the more honest fix. Either way, you decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strings have no [0]
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"café"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[E0277]: the type `str` cannot be indexed by `{integer}`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask what &lt;code&gt;s[0]&lt;/code&gt; should return and the refusal starts making sense. Rust strings are UTF-8 bytes; the é in "café" takes two of them, so the string is five bytes and four characters. "Element zero" has three defensible meanings: a byte, a codepoint, or the grapheme a reader would circle on paper. They agree on ASCII and diverge on everything else.&lt;/p&gt;

&lt;p&gt;Other languages resolve the ambiguity by picking silently. JavaScript indexes UTF-16 code units, so &lt;code&gt;"🦀"[0]&lt;/code&gt; is &lt;code&gt;"\ud83e"&lt;/code&gt;, half a surrogate pair that renders as garbage (Node 25.9.0; the string's &lt;code&gt;.length&lt;/code&gt; is 2 for one visible crab). C hands you a raw byte. Python gives you a whole character in O(1) and pays in memory: since PEP 393, each string is stored at the width of its widest codepoint.&lt;/p&gt;

&lt;p&gt;Rust makes you spell out which one you meant: &lt;code&gt;s.as_bytes()[0]&lt;/code&gt; for the byte (99), &lt;code&gt;s.chars().nth(0)&lt;/code&gt; for the character, and range slicing like &lt;code&gt;&amp;amp;s[0..1]&lt;/code&gt; for bytes with a boundary check; &lt;code&gt;&amp;amp;s[4..5]&lt;/code&gt; panics with "byte index 4 is not a char boundary; it is inside 'é'" instead of handing you half a character.&lt;/p&gt;

&lt;p&gt;The error message even suggests &lt;code&gt;.chars().nth()&lt;/code&gt; and &lt;code&gt;.bytes().nth()&lt;/code&gt;. rustc can see perfectly well what you're after; it wants the ambiguity resolved in your code rather than in its defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  255 + 1 has three answers
&lt;/h2&gt;

&lt;p&gt;This one is a ladder, not a single error. When the overflow is provable at compile time, it refuses outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: this arithmetic operation will overflow
 = note: `#[deny(arithmetic_overflow)]` on by default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the value opaque at runtime (parse it from a string) and the same addition panics in a debug build, "attempt to add with overflow," and prints 0 in release. That release wrap is defined two's-complement behavior, written down in RFC 560, never undefined.&lt;/p&gt;

&lt;p&gt;The C comparison matters here. Signed overflow in C isn't a wrap, it's undefined behavior (C11 6.5p5): the optimizer may assume it cannot happen and fold away the very check you wrote to catch it. Rust's split is a cost decision made honestly: checking every add costs a branch, so debug builds check everything and release builds wrap deterministically.&lt;/p&gt;

&lt;p&gt;If wrap is wrong for your domain, set &lt;code&gt;overflow-checks = true&lt;/code&gt; in the release profile, or name the semantics per call: &lt;code&gt;wrapping_add&lt;/code&gt;, &lt;code&gt;checked_add&lt;/code&gt;, &lt;code&gt;saturating_add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Eight bits genuinely cannot hold 256, so something has to give. Rust's stance is that the choice belongs in your code, and the debug panic is the reminder that you haven't written it down yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map that never ran
&lt;/h2&gt;

&lt;p&gt;This one is a warning, not an error, and it still cost me an afternoon: a &lt;code&gt;println!&lt;/code&gt; I'd tucked inside a &lt;code&gt;map&lt;/code&gt; for debugging never fired. The program ran clean, printed everything after it, and exited zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ran {x}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// prints only: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust iterator adapters are lazy. &lt;code&gt;map&lt;/code&gt; doesn't run the closure; it builds a small struct that waits for a consumer, a &lt;code&gt;for&lt;/code&gt; loop or &lt;code&gt;collect&lt;/code&gt; or &lt;code&gt;sum&lt;/code&gt;. Mine had no consumer, so the work never existed.&lt;/p&gt;

&lt;p&gt;The answer had been sitting in my terminal the whole time: "unused &lt;code&gt;Map&lt;/code&gt; that must be used: iterators are lazy and do nothing unless consumed." I'd scrolled past it the way JavaScript trained me to, because there &lt;code&gt;map&lt;/code&gt; runs eagerly and hands back a whole new array per stage.&lt;/p&gt;

&lt;p&gt;Rust's laziness is why a five-adapter chain compiles into one loop with no intermediate allocations. The price is that describing the work and doing the work are two separate moments, and this warning fires when your program only ever did the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Rust goes quiet
&lt;/h2&gt;

&lt;p&gt;I'd be selling you something if I stopped there, so here's the counterexample I hit the same week, in my Redis clone's append-only file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BufWriter&lt;/code&gt; does flush its buffer on scope exit; &lt;code&gt;Drop&lt;/code&gt; takes care of it. &lt;code&gt;Drop&lt;/code&gt;, though, has no way to return a &lt;code&gt;Result&lt;/code&gt;, so a failure in that final flush has nowhere to go and gets discarded. The standard library documents this openly and tells you to flush by hand first if you care. No compile error, no warning, nothing.&lt;/p&gt;

&lt;p&gt;Even a successful &lt;code&gt;flush()&lt;/code&gt; has only reached the kernel's page cache. Getting bytes onto the actual disk is &lt;code&gt;sync_all()&lt;/code&gt;, fsync under the hood, and nothing calls it for you.&lt;/p&gt;

&lt;p&gt;The type system polices what it can model, comparability and ownership above all. Durability policy, what your program is allowed to lose when the power dies, isn't in the types.&lt;/p&gt;

&lt;p&gt;Rust erases most of the C discipline I needed for the same file (my own buffering, &lt;code&gt;?&lt;/code&gt; on every write, close on scope exit) and leaves the two decisions that determine what a crash can take from you: when to flush, and when to sync. Those stay yours, unannounced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the trade
&lt;/h2&gt;

&lt;p&gt;Every refusal above is one question in different clothing: what should happen on the case you didn't think you had? Python, JavaScript, and C settle it for you at runtime, each in its own accidental way. Rust asks before the program exists and won't move until you answer. The friction is real; it also lands at the exact moment the bug would otherwise have shipped.&lt;/p&gt;

&lt;p&gt;Most of these came out of the Redis rebuild, and that project's durability layer, the append-only file where the quiet BufWriter almost bit me, is the next writeup.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>c</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Rebuilding my C Redis clone in Rust taught me more Rust than any tutorial</title>
      <dc:creator>Valentyn Kit</dc:creator>
      <pubDate>Mon, 06 Jul 2026 18:32:20 +0000</pubDate>
      <link>https://dev.to/valentynkit/rebuilding-my-c-redis-clone-in-rust-taught-me-more-rust-than-any-tutorial-3mpg</link>
      <guid>https://dev.to/valentynkit/rebuilding-my-c-redis-clone-in-rust-taught-me-more-rust-than-any-tutorial-3mpg</guid>
      <description>&lt;p&gt;I built a small Redis clone in C: a RESP parser, a command table, an append-only file for persistence. Recently I started building the same thing again in Rust, and rebuilding a project I had already finished has taught me more Rust than any from-scratch tutorial.&lt;/p&gt;

&lt;p&gt;The reason is simple. The second time, the design is already solved. I know what the AOF has to guarantee, what the command table dispatches, what the parser must reject. So none of my attention goes to &lt;em&gt;what&lt;/em&gt; to build. All of it goes to &lt;em&gt;how&lt;/em&gt; Rust wants it built.&lt;/p&gt;

&lt;p&gt;That turns the domain into a constant and the language into the only variable. Every difference I hit is pure signal about Rust, not noise about key-value stores.&lt;/p&gt;

&lt;p&gt;The first difference shows up before any logic runs. In C, I built the substrate first: my own dynamic strings, my own hashmap, my own linked list. Hundreds of lines before a single command worked. In Rust, &lt;code&gt;Vec&lt;/code&gt;, &lt;code&gt;String&lt;/code&gt;, and &lt;code&gt;HashMap&lt;/code&gt; are just there, so that whole layer disappears and I start at the actual command logic. A standard library quietly decides where your project even begins.&lt;/p&gt;

&lt;p&gt;The sharper difference is in dispatch. In C it is a switch with argument counts I check by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wrong arg count"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;CMD_SET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;do_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;CMD_GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;do_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="cm"&gt;/* forget a case and it is a runtime bug */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Rust the same dispatch is an enum and a match, and the compiler will not build until every case is handled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;      &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same dispatch. One version cannot ship the missing-case bug I actually shipped in C.&lt;/p&gt;

&lt;p&gt;If you already know a project cold, rebuild it in the language you are learning. You stop thinking about the problem and start feeling the language.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>c</category>
      <category>database</category>
      <category>programming</category>
    </item>
    <item>
      <title>From one blocking accept() to epoll: a C TCP server up the I/O ladder, measured</title>
      <dc:creator>Valentyn Kit</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:36:20 +0000</pubDate>
      <link>https://dev.to/valentynkit/from-one-blocking-accept-to-epoll-a-c-tcp-server-up-the-io-ladder-measured-3bi9</link>
      <guid>https://dev.to/valentynkit/from-one-blocking-accept-to-epoll-a-c-tcp-server-up-the-io-ladder-measured-3bi9</guid>
      <description>&lt;p&gt;I connected one client to a blocking TCP server and held the socket open without sending a single byte. Then I connected a second client and sent it a line of text. The second client sat there for 1.51 seconds with no reply. It got its echo back one millisecond after I closed the first connection.&lt;/p&gt;

&lt;p&gt;That 1.51 seconds is the reason the other six versions of this server exist.&lt;/p&gt;

&lt;p&gt;Last week I wrote up &lt;a href="https://valentynkit.com/blog/framework-knowledge-evaporates" rel="noopener noreferrer"&gt;why I rebuilt this server seven times&lt;/a&gt;: framework knowledge resets every few years, the layer underneath it compounds. That piece stayed at the level of outcomes. This one goes the other way, down into the code and the numbers.&lt;/p&gt;

&lt;p&gt;The claims that matter here are the kind you can read a hundred times without being able to derive them. "select is O(n)." "epoll only hands you the ready fds." I had read both for years. I wanted to make my own machine say them out loud.&lt;/p&gt;

&lt;p&gt;The target the whole exercise is built around is Dan Kegel's old &lt;a href="http://www.kegel.com/c10k.html" rel="noopener noreferrer"&gt;C10K problem&lt;/a&gt;: how do you serve ten thousand clients at once on one server? Each of the seven versions hits a wall, and the wall is what names the next one.&lt;/p&gt;

&lt;p&gt;The whole thing is one echo server written seven times, no libraries beyond libc, &lt;a href="https://github.com/valentynkit/building-tcp-servers-in-c" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Every number below is from running it on macOS (Apple clang 21, darwin 25.4) on 2026-06-29. The binaries are built with AddressSanitizer and UBSan on, so read the absolute microseconds loosely. The structure is what holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 01: blocking, and the 1.5 second stall
&lt;/h2&gt;

&lt;p&gt;The first server is the one everybody writes first. Accept a connection, talk to it, close it, accept the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;client_fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_fd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;handle_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;handle_client&lt;/code&gt; loops on &lt;code&gt;read&lt;/code&gt; until the client hangs up. Both &lt;code&gt;accept&lt;/code&gt; and &lt;code&gt;read&lt;/code&gt; block: when there is nothing to do, the thread sleeps in the kernel. That is good for idle cost and fatal for everything else. While the server is parked in &lt;code&gt;read&lt;/code&gt; waiting on client A, client B is sitting in the kernel's backlog queue, accepted by TCP but not yet by your code. The server never gets to it.&lt;/p&gt;

&lt;p&gt;That is the stall I measured at the top. Client B waited 1.51 seconds, the exact length of time I held client A open, and was served the instant A closed. One idle connection freezes every other connection behind it. The file comment in the repo says it plainly: "Open two clients at once and watch the second one hang." I wanted the number, so I held the first one open on purpose.&lt;/p&gt;

&lt;p&gt;The good news is the idle cost. With no clients connected, this server sits at 0.0% CPU. It is asleep in &lt;code&gt;accept&lt;/code&gt;. Hold onto that, because the next version throws it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 02: non-blocking, and a pegged core
&lt;/h2&gt;

&lt;p&gt;The obvious fix: stop blocking. Put every socket in &lt;code&gt;O_NONBLOCK&lt;/code&gt; mode and the syscalls return immediately with &lt;code&gt;EAGAIN&lt;/code&gt; instead of sleeping. Now one thread can walk all its clients, try each one, and skip the ones with nothing ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;accept_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* a client was swapped into i, re-check it */&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. No client can freeze the others anymore. It also has a problem you can hear with a fan. The loop never sleeps.&lt;/p&gt;

&lt;p&gt;The two calls hide the busy-work: &lt;code&gt;accept_client&lt;/code&gt; fires a non-blocking &lt;code&gt;accept&lt;/code&gt;, and &lt;code&gt;handle_client&lt;/code&gt; a non-blocking &lt;code&gt;read&lt;/code&gt;. When every client is idle both return &lt;code&gt;EAGAIN&lt;/code&gt;, the outer loop runs them again, and with nothing to do it spins as fast as the core allows, millions of &lt;code&gt;EAGAIN&lt;/code&gt;s a second.&lt;/p&gt;

&lt;p&gt;I measured the same idle server two ways. Blocking, phase 01, no clients: 0.0% CPU. Non-blocking, phase 02, no clients: 98 to 99% CPU. A whole core burned to accomplish nothing. That is not a tuning problem, it is the design. Polling means asking "anything yet?" forever, and the answer is almost always no.&lt;/p&gt;

&lt;p&gt;(The &lt;code&gt;i--&lt;/code&gt; in that loop is a small trap worth pointing at. When a client disconnects, the code swaps the last client into its slot to keep the array dense, then decrements &lt;code&gt;i&lt;/code&gt; so the loop re-checks the slot the swap just refilled. Miss that and you skip a client every time one leaves.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 03: select, and two scars from the 1980s
&lt;/h2&gt;

&lt;p&gt;Phase 02 had the right shape and the wrong instruction. We do not want to ask "anything yet?" in a loop. We want to tell the kernel "wake me when one of these is ready" and go to sleep until it is. That instruction is &lt;code&gt;select&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* select() overwrites the set, so hand it a fresh copy each loop. */&lt;/span&gt;
&lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="n"&gt;readfds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_fds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_fd&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idle core comes back instantly. The server sleeps in &lt;code&gt;select&lt;/code&gt; until a client actually does something, exactly like phase 01 slept in &lt;code&gt;accept&lt;/code&gt;, except now it is waiting on the whole set at once. This is the real idea in the whole ladder: readiness notification. One wait, one wakeup, for many connections.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;select&lt;/code&gt; carries the idea wrapped in two pieces of 1980s baggage. The first is in that comment: &lt;code&gt;select&lt;/code&gt; overwrites the set you give it with the subset that is ready, so you keep a master copy and hand it a throwaway duplicate every single loop. The second is harder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;FD_SETSIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fd %d &amp;gt;= FD_SETSIZE, rejecting&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fd set is a fixed-size bitmask. On this machine &lt;code&gt;FD_SETSIZE&lt;/code&gt; is 1024, which I confirmed by compiling a one-line program that prints it. A connection whose file descriptor lands at or above 1024 cannot be represented in the set at all, so the server has to refuse it.&lt;/p&gt;

&lt;p&gt;On top of the cap, every wakeup scans the whole bitmap up to &lt;code&gt;max_fd&lt;/code&gt;, so the cost is O(n) in the number of connections you are watching, whether one of them is active or a thousand.&lt;/p&gt;

&lt;p&gt;That is the end of the road for C10K on &lt;code&gt;select&lt;/code&gt;. The bitmask caps you at 1024, a tenth of the way to the goal, and the per-wakeup scan would tax you even if it did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 04: poll, one scar gone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;poll&lt;/code&gt; is &lt;code&gt;select&lt;/code&gt; with the bitmask swapped for an array of &lt;code&gt;struct pollfd&lt;/code&gt;, one entry per connection. That removes the &lt;code&gt;FD_SETSIZE&lt;/code&gt; cap: the array is as big as you make it.&lt;/p&gt;

&lt;p&gt;It does not remove the other scar. The kernel still walks every entry on every call, and you still hand it the full list each time. Better ceiling, same complexity: O(n) per wakeup, still proportional to the connections you hold, not the ones doing anything.&lt;/p&gt;

&lt;p&gt;To actually fix the cost, the kernel has to remember what you care about between calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phases 05 and 06: kqueue and epoll, register once
&lt;/h2&gt;

&lt;p&gt;This is the jump. Instead of handing the kernel the entire list of connections on every wakeup, you register each connection once, and from then on the kernel hands &lt;em&gt;you&lt;/em&gt; back only the ones that are ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;kevent&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kevent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;ident&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;server_fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;accept_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server_fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;                 &lt;span class="n"&gt;handle_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kevent&lt;/code&gt; returns &lt;code&gt;n&lt;/code&gt;, the number of ready events, and you loop over exactly those. The repo comment is the clearest one-line statement of the payoff in the whole project: "with 10,000 idle connections and one active, kevent() hands you exactly that one." The work per wakeup is O(ready), not O(registered). The fd cap is gone too.&lt;/p&gt;

&lt;p&gt;I measured the readiness behavior, not the throughput. Three clients connected at once, each sending a line, each got its echo back in 0.1 to 0.2 milliseconds, overlapping, not serialized. That is the property phase 01 could not have at any speed: progress on many connections inside one loop, with the loop asleep whenever nothing is happening.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kqueue&lt;/code&gt; is the BSD and macOS interface. Linux spells the identical idea with different syscalls, which is &lt;code&gt;epoll&lt;/code&gt;. They are not two steps, they are one idea on two kernels, and the repo lays the mapping out side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kqueue()                     -&amp;gt;  epoll_create1()
EV_SET(... EVFILT_READ ...)  -&amp;gt;  struct epoll_event { .events=EPOLLIN }
kevent(... EV_ADD ...)       -&amp;gt;  epoll_ctl(EPOLL_CTL_ADD)
kevent(... EV_DELETE ...)    -&amp;gt;  epoll_ctl(EPOLL_CTL_DEL)
kevent(... &amp;amp;events ...)      -&amp;gt;  epoll_wait()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The epoll version does not compile on macOS, there is no &lt;code&gt;&amp;lt;sys/epoll.h&amp;gt;&lt;/code&gt;, so I built and ran it in a Linux container (&lt;code&gt;gcc:14&lt;/code&gt;) to confirm it is the same program. It is. The loop is byte-for-byte the kqueue loop with the five calls above renamed.&lt;/p&gt;

&lt;p&gt;Once you have written one, you have written both. You have also written the thing every async runtime is sitting on. Go's netpoller, Node's libuv, tokio through mio: a loop that asks the kernel what is ready, handles exactly that, and goes back to sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 07: the byte stream that isn't messages
&lt;/h2&gt;

&lt;p&gt;The first six servers all echo raw bytes, which lets them dodge the problem that makes a real protocol hard: TCP is a stream, not a sequence of messages. One &lt;code&gt;read&lt;/code&gt; can return half of what the client sent, or three messages glued together. If your server assumes one &lt;code&gt;read&lt;/code&gt; equals one message, it works on localhost and falls apart the first time a packet boundary lands somewhere inconvenient.&lt;/p&gt;

&lt;p&gt;Phase 07 puts a length prefix on every message and extracts whole frames out of a per-client buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;FRAME_HDR_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;net_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;net_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FRAME_HDR_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;payload_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ntohl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MSG_MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;remove_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;FRAME_HDR_SIZE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;payload_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="cm"&gt;/* frame not fully arrived yet */&lt;/span&gt;

    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FRAME_HDR_SIZE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;payload_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;queue_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="cm"&gt;/* write buffer full, resume after drain */&lt;/span&gt;

    &lt;span class="n"&gt;memmove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there are the difference between a toy and a server.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;if (c-&amp;gt;len &amp;lt; FRAME_HDR_SIZE + payload_len) return;&lt;/code&gt; is framing: a partial message is not an error, it is just not here yet, so you keep the bytes and wait for the rest.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;if (!queue_write(...)) return;&lt;/code&gt; is backpressure: when a client reads slowly and its write buffer fills, the server stops pulling new frames off the read side instead of buffering without bound. A slow client throttles itself rather than growing the server's memory until it dies.&lt;/p&gt;

&lt;p&gt;I did not eyeball this one. The repo has a Python client that hammers the framing rules, and it passes: a single message, three pipelined into one send, a 4000-byte payload, and an oversized one that the server correctly drops the connection over. That last test is the protocol defending itself, which is the part you only get once you stop pretending the stream is already messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;I expected the headline jump to be &lt;code&gt;select&lt;/code&gt; to &lt;code&gt;epoll&lt;/code&gt;, the efficiency one, the thing the blog posts are about. It was not the one that reorganized my head.&lt;/p&gt;

&lt;p&gt;The jump that did was phase 02 to phase 03, non-blocking to &lt;code&gt;select&lt;/code&gt;. It is the move from "I ask the world, constantly, whether anything has happened" to "the kernel wakes me when something has." Those feel like the same architecture with a faster inner loop. They are not.&lt;/p&gt;

&lt;p&gt;One pegs a core and scales by spending more CPU; the other sleeps and scales by spending less. Everything after phase 03, poll, kqueue, epoll, is the same idea getting cheaper. Phase 03 is where the idea arrives.&lt;/p&gt;

&lt;p&gt;I had the syscalls filed under "non-blocking I/O" as one bucket for years. They are two different answers to two different questions, and I could not have told you that before I watched the CPU number drop from 99 to nothing by changing which call the loop sleeps in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The framing version is roughly where this stops being a toy and starts being a thing with opinions: a wire format, a memory bound, a client it is willing to hang up on. It is also where C runs out of story to tell.&lt;/p&gt;

&lt;p&gt;The next interesting question is not another syscall, it is the abstraction tower built on top of this one. I rebuilt the same ladder in Rust, raw &lt;code&gt;unsafe&lt;/code&gt; libc calls up through mio up through tokio, specifically to see what each layer buys and what it hides. That is the part C structurally cannot show you, and it is what I am writing up next.&lt;/p&gt;

</description>
      <category>c</category>
      <category>systems</category>
      <category>networking</category>
      <category>linux</category>
    </item>
    <item>
      <title>Adding io::ErrorKind::TooManyOpenFiles to Rust</title>
      <dc:creator>Valentyn Kit</dc:creator>
      <pubDate>Sat, 27 Jun 2026 17:56:58 +0000</pubDate>
      <link>https://dev.to/valentynkit/adding-ioerrorkindtoomanyopenfiles-to-rust-4h0k</link>
      <guid>https://dev.to/valentynkit/adding-ioerrorkindtoomanyopenfiles-to-rust-4h0k</guid>
      <description>&lt;p&gt;This is my second merged contribution to rust-lang, and like the first it lives in the low-level I/O layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;When a process runs out of file descriptors, the OS returns &lt;code&gt;EMFILE&lt;/code&gt; (the per-process limit) or &lt;code&gt;ENFILE&lt;/code&gt; (the system-wide one). Until recently, Rust's standard library decoded both into &lt;code&gt;io::ErrorKind::Uncategorized&lt;/code&gt;. If you wanted to react to "too many open files" specifically, you couldn't match on the kind. You had to drop to the raw errno:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="nf"&gt;.raw_os_error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EMFILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// back off and retry&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, but it leaks a platform detail into code that only wanted a category.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rust-lang/rust/pull/158326" rel="noopener noreferrer"&gt;PR #158326&lt;/a&gt; adds a dedicated variant to &lt;code&gt;io::ErrorKind&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; pub enum ErrorKind {
     #[unstable(feature = "io_error_inprogress", issue = "130840")]
     InProgress,
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+    /// The process or the whole system has reached its limit on
+    /// the number of open files or sockets.
+    #[unstable(feature = "io_error_too_many_open_files", issue = "158319")]
+    TooManyOpenFiles,
+
&lt;/span&gt;     // "Unusual" error kinds ...
     Uncategorized,
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and wires it into the per-platform decode tables, so both errnos map to it on unix and wasi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EMFILE&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ENFILE&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TooManyOpenFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the matching &lt;code&gt;ERROR_TOO_MANY_OPEN_FILES&lt;/code&gt; and &lt;code&gt;WSAEMFILE&lt;/code&gt; on Windows. So now the category is first class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="nf"&gt;.kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TooManyOpenFiles&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;back_off_and_retry&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The variant is unstable for now (it implements &lt;a href="https://github.com/rust-lang/libs-team/issues/818" rel="noopener noreferrer"&gt;ACP libs-team#818&lt;/a&gt;), behind &lt;code&gt;#![feature(io_error_too_many_open_files)]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this corner
&lt;/h2&gt;

&lt;p&gt;It is a small change, 13 lines. Reliability work usually is. This is the second PR I have landed in rust-lang's I/O code. The first documented the transient errors &lt;code&gt;TcpListener::accept&lt;/code&gt; can hand you, the kind that are easy to treat as fatal by mistake. I am keeping at it on the low-level side, the layer the rest of the ecosystem stands on.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>systems</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Framework knowledge evaporates. Low-level knowledge compounds.</title>
      <dc:creator>Valentyn Kit</dc:creator>
      <pubDate>Thu, 25 Jun 2026 11:11:10 +0000</pubDate>
      <link>https://dev.to/valentynkit/framework-knowledge-evaporates-low-level-knowledge-compounds-4acj</link>
      <guid>https://dev.to/valentynkit/framework-knowledge-evaporates-low-level-knowledge-compounds-4acj</guid>
      <description>&lt;p&gt;I learned async three times before it stuck. C# Tasks. Go goroutines. Rust tokio. Each time I was shipping within a week. Each time, a year later at a different job, I was asking the same questions again.&lt;/p&gt;

&lt;p&gt;I hadn't forgotten the API. I'd learned the interface, not the mechanism.&lt;/p&gt;

&lt;p&gt;What changed it: I built a TCP echo server in C from blocking sockets up through a kqueue/epoll event loop, one I/O model at a time, with no libraries beyond libc. Seven phases, seven runnable programs, each motivated by a concrete ceiling the previous one hit.&lt;/p&gt;

&lt;p&gt;By the end, I understood what problem each design decision in tokio was solving. Go's netpoller and Node's libuv looked like two spellings of the same idea. Async had a shape I could hold. And I haven't needed to rebuild that mental model since.&lt;/p&gt;

&lt;p&gt;That's the trade I kept not making: framework knowledge gets you productive fast and resets every few years; low-level knowledge costs more upfront and compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got here
&lt;/h2&gt;

&lt;p&gt;The fourth time I learned async I was three stacks in. C# Tasks, then Go goroutines, then Rust's tokio. Each time it took about a week to get up to speed, and each time I thought I understood it. Moving to the next job, the understanding had reset in ways I didn't expect: I could explain the primitives but only in the vocabulary of the most recent stack. Strip the framework's names away and I was less certain than six years should have made me.&lt;/p&gt;

&lt;p&gt;Same thing with the other primitives I kept reaching for: idempotency, back-pressure, state machines. At each stop they showed up in different forms. I recognized them. I just didn't understand them in a way that would survive moving somewhere new.&lt;/p&gt;

&lt;p&gt;At some point that stopped feeling like a normal learning curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven-phase TCP server
&lt;/h2&gt;

&lt;p&gt;There's a document from 2003 by Dan Kegel called "The C10K Problem." The question it poses: how do you serve ten thousand clients with one server? The answer turns out to be very careful, and not what you'd first try.&lt;/p&gt;

&lt;p&gt;I built through it phase by phase.&lt;/p&gt;

&lt;p&gt;Phase 01 is the obvious approach: &lt;code&gt;accept&lt;/code&gt; in a loop, &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;, one client at a time. It works, and it blocks on every syscall, so one slow client holds the whole server. That's the ceiling.&lt;/p&gt;

&lt;p&gt;Phase 02: make the sockets non-blocking. Now &lt;code&gt;read&lt;/code&gt; returns &lt;code&gt;EAGAIN&lt;/code&gt; instead of sleeping, so one thread can check multiple clients. But you're busy-polling every file descriptor in a loop, burning 100% CPU even when nothing is happening. The wrong direction.&lt;/p&gt;

&lt;p&gt;Phase 03: &lt;code&gt;select&lt;/code&gt;. Tell the kernel "wake me when something on this list is ready," and it does. One wait, one wakeup. This is the idea: readiness notification. The problem is &lt;code&gt;FD_SETSIZE&lt;/code&gt;, which caps you at 1024 file descriptors on most platforms, and the kernel still scans the entire bitmap O(n) on every call.&lt;/p&gt;

&lt;p&gt;Phase 04: &lt;code&gt;poll&lt;/code&gt;. No bitmap cap, same O(n) cost. You still rebuild the descriptor list on every call.&lt;/p&gt;

&lt;p&gt;Phase 05 (macOS/BSD) and Phase 06 (Linux): kqueue and epoll. Register interest once; the kernel tells you only what's ready. O(ready), not O(registered).&lt;/p&gt;

&lt;p&gt;Phase 07: framing and backpressure. Length-prefixed messages, per-client write buffers that stop reading when the output side is full. Now you have the protocol problem, not just the I/O problem.&lt;/p&gt;

&lt;p&gt;Seven phases, each one runnable, each one the answer to the ceiling the previous one hit. No dependencies beyond libc. The whole thing is at &lt;a href="https://github.com/valentynkit/building-tcp-servers-in-c" rel="noopener noreferrer"&gt;github.com/valentynkit/building-tcp-servers-in-c&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I was wrong about
&lt;/h2&gt;

&lt;p&gt;I thought understanding non-blocking I/O was about knowing the right syscalls: &lt;code&gt;select&lt;/code&gt;, then &lt;code&gt;poll&lt;/code&gt;, then &lt;code&gt;epoll_create1&lt;/code&gt; + &lt;code&gt;epoll_ctl&lt;/code&gt; + &lt;code&gt;epoll_wait&lt;/code&gt;. Get the signatures right, ship the server.&lt;/p&gt;

&lt;p&gt;What I actually needed was causation: why did each model replace the previous one, what ceiling did it hit, what does the next one do differently? That's what transfers. The syscall names are spellings; the causation is the thing.&lt;/p&gt;

&lt;p&gt;I'd read "epoll is more efficient than select" dozens of times before building this. I understood the sentence. I didn't understand it in a way that let me derive it. After running my phase 03 server into the &lt;code&gt;FD_SETSIZE&lt;/code&gt; limit during a test, I understood it in a way that doesn't need reading again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What compounded
&lt;/h2&gt;

&lt;p&gt;The model from those seven phases: a server that scales is a loop that asks the kernel "what's ready now," processes exactly those clients, and yields back. Everything else (Go goroutines backed by the netpoller, tokio tasks backed by mio backed by epoll, Node's event loop backed by libuv) is a scheduling layer on top of that kernel primitive.&lt;/p&gt;

&lt;p&gt;When I read the mio source later, I recognized what it was doing. When I read about Go's netpoller, same. When I saw libuv's handle loop, same. Because I'd built the thing they're all built on, not because I studied each one until it made sense.&lt;/p&gt;

&lt;p&gt;The same transfer shows up in production. We model every transaction as an explicit state machine: every state durable, every transition idempotent, so a crash or a chain reorg resumes from the last clean step instead of corrupting money-bearing state. I'd seen that same pattern in a message queue pipeline: the retry path where a downstream failure had to resume without reprocessing what already committed. Same primitive, different altitude. You recognize it from below, or you don't recognize it at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;I'm rebuilding the foundations from scratch: TCP servers, allocators, storage engines, event loops. To understand what the systems I depend on are actually doing, written in public as I go.&lt;/p&gt;

&lt;p&gt;I'm not a kernel hacker. I'm a backend engineer who got tired of relearning the same ideas at every new framework, went down to find where they live, and is climbing back up. The bugs stay in. The dead ends stay in. The parts I'm still working out stay in.&lt;/p&gt;

&lt;p&gt;Current position: TCP servers and simple allocators done. Next I want to understand what a memory allocator is actually trading off, because I've been reaching for &lt;code&gt;malloc&lt;/code&gt; for six years and that's started to bother me.&lt;/p&gt;

&lt;p&gt;Code is at &lt;a href="https://github.com/valentynkit" rel="noopener noreferrer"&gt;github.com/valentynkit&lt;/a&gt;. The TCP server is above; the allocator is next.&lt;/p&gt;

</description>
      <category>systems</category>
      <category>c</category>
      <category>networking</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
