<?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: Toc am</title>
    <description>The latest articles on DEV Community by Toc am (@amtocbot).</description>
    <link>https://dev.to/amtocbot</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%2F4051140%2F0072bb72-7db2-45a7-a59b-7eef8697f644.png</url>
      <title>DEV Community: Toc am</title>
      <link>https://dev.to/amtocbot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amtocbot"/>
    <language>en</language>
    <item>
      <title>Retry Storms in C#: Exponential Backoff and Jitter with HttpClient</title>
      <dc:creator>Toc am</dc:creator>
      <pubDate>Tue, 28 Jul 2026 13:20:34 +0000</pubDate>
      <link>https://dev.to/amtocbot/retry-storms-in-c-exponential-backoff-and-jitter-with-httpclient-lah</link>
      <guid>https://dev.to/amtocbot/retry-storms-in-c-exponential-backoff-and-jitter-with-httpclient-lah</guid>
      <description>&lt;p&gt;The database blipped for four seconds. The incident lasted forty minutes.&lt;/p&gt;

&lt;p&gt;Nothing in the timeline explains the gap except the retry code your own clients are running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The junior version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.internal/orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsStringAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// transient. it'll come back.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Client log during the incident:&lt;/span&gt;
&lt;span class="c1"&gt;// 14:02:11.418  attempt failed&lt;/span&gt;
&lt;span class="c1"&gt;// 14:02:11.419  attempt failed&lt;/span&gt;
&lt;span class="c1"&gt;// 14:02:11.421  attempt failed&lt;/span&gt;
&lt;span class="c1"&gt;// ...about one attempt per millisecond, per client, across 5,000 clients.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it breaks
&lt;/h2&gt;

&lt;p&gt;Start with the part nobody expects: &lt;strong&gt;failure is fast&lt;/strong&gt;. A successful call does DNS, a TLS handshake, a query, and serialisation — tens of milliseconds. A refused connection comes back as a TCP RST in under a millisecond. A 503 from the load balancer never reaches the application at all. So the loop's period is set by the &lt;em&gt;fastest&lt;/em&gt; path through the system, and the fastest path is the broken one. The moment the service degrades, every client starts calling it harder than it ever did while healthy.&lt;/p&gt;

&lt;p&gt;Now multiply. Normal load is one request per user action. Under failure it is a continuous stream per client, and 5,000 clients pointed at a service sized for a few thousand requests per second will offer it millions. Those attempts are not free just because they fail: each one still costs a socket accept, a TLS handshake, a thread-pool work item, and a slot in the request queue on the server side. The capacity needed to drain the backlog is exactly the capacity being spent rejecting new arrivals.&lt;/p&gt;

&lt;p&gt;That is the whole failure mode. At 14:02:15 the original trigger is gone — the failover finished, the slow query completed — and the system stays down anyway, because the retry traffic is now the load. The service has two stable states, and your clients are pinning it in the wrong one. Recovery requires the offered load to drop, which is the one thing a retry loop will never do.&lt;/p&gt;

&lt;h3&gt;
  
  
  A detour worth taking: a fixed delay is not backoff
&lt;/h3&gt;

&lt;p&gt;This is the fix everyone reaches for first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;catch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// there. no more spinning.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does stop the spin, and it drops each client from a thousand attempts per second to one. It also blocks a thread-pool thread inside async code, which is its own problem. But the herd survives.&lt;/p&gt;

&lt;p&gt;The clients were &lt;em&gt;synchronised by the outage itself&lt;/em&gt;. They all failed at 14:02:11.418, so they all wake at 14:02:12.418, and again at 14:02:13.418. The failure event acted as a clock-sync pulse. What the server sees is not a steady 5,000 requests per second — it is all 5,000 arriving at the same instant, once a second, with dead air in between. Peak concurrency is what topples a connection pool, and peak concurrency is unchanged. You have made the graph prettier without moving the number that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The senior version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetWithRetryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;baseDelay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;maxDelay&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&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;attempt&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&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="nf"&gt;IsTransient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&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;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;
                                            &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Exponential ceiling, then pick uniformly *below* it. That second&lt;/span&gt;
        &lt;span class="c1"&gt;// step is the jitter, and it is the part that scatters the herd.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ceiling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxDelay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;baseDelay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMilliseconds&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextDouble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsTransient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpStatusCode&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestTimeout&lt;/span&gt;       &lt;span class="c1"&gt;// 408&lt;/span&gt;
    &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TooManyRequests&lt;/span&gt;   &lt;span class="c1"&gt;// 429&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="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exponential growth thins the traffic over time. The uniform draw spreads each wave across the whole interval, so two clients that failed in the same millisecond come back at two random points inside a 200 ms window instead of together — and that window doubles on every attempt after, to a 20-second ceiling. &lt;code&gt;Random.Shared&lt;/code&gt; is thread-safe, so you do not need one &lt;code&gt;Random&lt;/code&gt; per caller.&lt;/p&gt;

&lt;p&gt;Siblings worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Polly v8: &lt;code&gt;new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions { BackoffType = DelayBackoffType.Exponential, UseJitter = true })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AddStandardResilienceHandler()&lt;/code&gt; from &lt;code&gt;Microsoft.Extensions.Http.Resilience&lt;/code&gt;, which wires retry, jitter, a circuit breaker and a total timeout into a named &lt;code&gt;HttpClient&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the server sends &lt;code&gt;Retry-After&lt;/code&gt; (&lt;code&gt;response.Headers.RetryAfter&lt;/code&gt;), honour it — then add jitter on top anyway, or every client obeying it wakes in lockstep again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When backoff isn't enough
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Retry is only safe on idempotent operations.&lt;/strong&gt; A timeout does not tell you the call failed; it tells you the &lt;em&gt;response&lt;/em&gt; was lost. The charge may already be posted. &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; are idempotent by contract; &lt;code&gt;POST&lt;/code&gt; is not. Retrying one needs an idempotency key the server dedupes against, not a smarter delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classify before you retry.&lt;/strong&gt; A 400, 401 or 404 will fail identically on attempt five. Retrying it burns the caller's latency budget for a guaranteed failure. That is what &lt;code&gt;IsTransient&lt;/code&gt; above is for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cap elapsed time, not attempt count.&lt;/strong&gt; Five attempts with a 30-second HTTP timeout each, plus backoff, is over two minutes — long after the browser gave up. Wrap the whole loop in one &lt;code&gt;CancellationTokenSource(TimeSpan.FromSeconds(10))&lt;/code&gt; and let it cut the retries short.&lt;/p&gt;

&lt;p&gt;And when the dependency is genuinely down rather than flaky, backoff still has every client politely probing a corpse. That is a circuit breaker's job: stop calling at all, and check back with one request instead of all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Backoff spreads out &lt;em&gt;your&lt;/em&gt; retries. Jitter spreads out &lt;em&gt;everyone else's&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Without the second one, you haven't built resilience — you've scheduled a DDoS against yourself, once per second, right on time.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/RHEUaEdMEg8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;I post one of these every day — the same problem solved the way that works and the way that lasts.&lt;br&gt;
One senior tip a week, by email: &lt;a href="https://seniorvsjunior.higgsfield.app" rel="noopener noreferrer"&gt;https://seniorvsjunior.higgsfield.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why File.ReadAllLines Throws OutOfMemoryException on a 10 GB File in C#</title>
      <dc:creator>Toc am</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:31:15 +0000</pubDate>
      <link>https://dev.to/amtocbot/why-filereadalllines-throws-outofmemoryexception-on-a-10-gb-file-in-c-1d28</link>
      <guid>https://dev.to/amtocbot/why-filereadalllines-throws-outofmemoryexception-on-a-10-gb-file-in-c-1d28</guid>
      <description>&lt;p&gt;The job runs fine against the 200 MB sample. It dies in production against the real 10 GB log, before a single line is processed.&lt;/p&gt;

&lt;p&gt;Here is the version almost everyone writes first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The junior version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"C:\data\events.log"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Unhandled exception. System.OutOfMemoryException: Insufficient memory&lt;/span&gt;
&lt;span class="c1"&gt;// to continue the execution of the program.&lt;/span&gt;
&lt;span class="c1"&gt;//    at System.Collections.Generic.List`1.AddWithResize(String)&lt;/span&gt;
&lt;span class="c1"&gt;//    at System.IO.File.ReadAllLines(String path)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing was printed. The process never reached the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it breaks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;File.ReadAllLines&lt;/code&gt; is not a reading primitive, it is a materialising one. Internally it opens a &lt;code&gt;StreamReader&lt;/code&gt;, appends every &lt;code&gt;ReadLine()&lt;/code&gt; result to a &lt;code&gt;List&amp;lt;string&amp;gt;&lt;/code&gt;, and returns &lt;code&gt;list.ToArray()&lt;/code&gt;. The method only returns once the last line of the file is in memory.&lt;/p&gt;

&lt;p&gt;Three things multiply the cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UTF-16.&lt;/strong&gt; .NET strings hold two bytes per character. Your 10 GB of ASCII on disk becomes ~20 GB of character data on the heap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-object overhead.&lt;/strong&gt; Each line is a separate object: 8 bytes of header, 8 for the method table pointer, 4 for the length, a null terminator, padded to an 8-byte boundary — call it ~24 bytes per line — plus 8 bytes for its slot in the array. At 100-byte lines that's ~107 million objects and roughly 23 GB of heap for a 10 GB file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The doubling.&lt;/strong&gt; The backing array of the &lt;code&gt;List&amp;lt;string&amp;gt;&lt;/code&gt; grows by repeated doubling, and every array above 85,000 bytes lands on the Large Object Heap, which is not compacted by default. You allocate, abandon and fragment your way up through 100 MB, 200 MB, 400 MB of reference arrays, then &lt;code&gt;ToArray()&lt;/code&gt; allocates one more full-size copy alongside the last one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GC cannot help. Every one of those strings is reachable from the array you asked for, so nothing is garbage. The allocation that finally fails is arbitrary — the exception surfaces wherever the heap happened to run out.&lt;/p&gt;

&lt;h3&gt;
  
  
  A detour worth taking: &lt;code&gt;foreach&lt;/code&gt; does not make it lazy
&lt;/h3&gt;

&lt;p&gt;This is the most common wrong fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// still OOMs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;A &lt;code&gt;foreach&lt;/code&gt; evaluates its collection expression &lt;em&gt;once&lt;/em&gt;, in full, before the first iteration. &lt;code&gt;File.ReadAllLines(path)&lt;/code&gt; runs to completion and hands back a finished &lt;code&gt;string[]&lt;/code&gt;; only then does enumeration start. The loop shape tells you nothing. The return type does: &lt;code&gt;string[]&lt;/code&gt; is a result, &lt;code&gt;IEnumerable&amp;lt;string&amp;gt;&lt;/code&gt; is a plan.&lt;/p&gt;

&lt;p&gt;The other reach is &lt;code&gt;File.ReadAllText(path).Split('\n')&lt;/code&gt;, which is strictly worse. A &lt;code&gt;string&lt;/code&gt; is hard-capped at 1,073,741,791 characters — roughly 1.07 billion, no matter how much RAM the box has — so the 10 GB read fails on the string itself, before &lt;code&gt;Split&lt;/code&gt; runs. If it did run, &lt;code&gt;Split&lt;/code&gt; allocates every substring &lt;em&gt;on top of&lt;/em&gt; the original, and on a CRLF file it leaves a stray &lt;code&gt;\r&lt;/code&gt; on the end of every line.&lt;/p&gt;

&lt;p&gt;And "just give the box more RAM" is not a fix, it is a subscription. It makes your memory ceiling a function of your input size, so you pay more every quarter and still fall over the day someone hands you a bigger file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The senior version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"C:\data\events.log"&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;File.ReadLines&lt;/code&gt; returns a lazy iterator over a &lt;code&gt;StreamReader&lt;/code&gt;. Each &lt;code&gt;MoveNext()&lt;/code&gt; decodes exactly one line and yields it; the &lt;code&gt;foreach&lt;/code&gt; disposes the enumerator — and with it the reader and the file handle — when the loop ends. Peak memory is one line plus the reader's buffers, whether the file is 10 MB or 10 TB. The 10 GB case now runs in a few megabytes.&lt;/p&gt;

&lt;p&gt;Because it is a real &lt;code&gt;IEnumerable&amp;lt;string&amp;gt;&lt;/code&gt;, LINQ composes over it without buffering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;firstTen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Siblings worth knowing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// explicit encoding&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLinesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// .NET 7+&lt;/span&gt;
    &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Full control: bigger buffer, sequential-scan hint to the OS.&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileAccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileShare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SequentialScan&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadLine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When streaming isn't enough
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;File.ReadLines&lt;/code&gt; opens the file when you call it, not on the first &lt;code&gt;MoveNext()&lt;/code&gt; — the handle is live before the loop starts. So don't return &lt;code&gt;File.ReadLines(...)&lt;/code&gt; out of a method that owns the path and expect the handle to be closed: it stays open until someone finishes enumerating or disposes the enumerator, a sequence nobody ever enumerates holds it until the finalizer runs, and with the default &lt;code&gt;FileShare.Read&lt;/code&gt; it blocks writers on Windows for the whole run.&lt;/p&gt;

&lt;p&gt;Enumerating twice reads the file twice. That is 10 GB of I/O per pass, and if the file changed in between, the two passes disagree. And the moment you call &lt;code&gt;.OrderBy(...)&lt;/code&gt;, &lt;code&gt;.ToList()&lt;/code&gt; or &lt;code&gt;.GroupBy(...)&lt;/code&gt; on the sequence, you have re-materialised the whole thing and bought the original bug back. &lt;code&gt;.Count()&lt;/code&gt; is the one that looks like those and isn't: it walks the sequence and keeps nothing, so it costs you a full 10 GB pass in time while memory stays flat.&lt;/p&gt;

&lt;p&gt;If you genuinely need random access or several passes, streaming alone won't carry you: build an index of line offsets on one pass, sort externally, or load only the projection you need. Streaming buys you constant memory over a single forward pass — that is the whole contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ReadAllLines&lt;/code&gt; gives you an array; &lt;code&gt;ReadLines&lt;/code&gt; gives you a promise. Only one of them fits in RAM.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Sfy6ry1-ZvU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;I post one of these every day — the same problem solved the way that works and the way that lasts.&lt;br&gt;
One senior tip a week, by email: &lt;a href="https://seniorvsjunior.higgsfield.app" rel="noopener noreferrer"&gt;https://seniorvsjunior.higgsfield.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How LLMs Generate Text — one token at a time</title>
      <dc:creator>Toc am</dc:creator>
      <pubDate>Tue, 28 Jul 2026 11:00:38 +0000</pubDate>
      <link>https://dev.to/amtocbot/how-llms-generate-text-one-token-at-a-time-57j2</link>
      <guid>https://dev.to/amtocbot/how-llms-generate-text-one-token-at-a-time-57j2</guid>
      <description>&lt;p&gt;&lt;em&gt;One token at a time — a very well-read autocomplete.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A large language model doesn't plan a whole answer up front. It predicts the next token from everything so far, appends it, and repeats — with attention letting it weigh which earlier words matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each token appears
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tokenize.&lt;/strong&gt; Text is split into subword tokens and mapped to numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed.&lt;/strong&gt; Each token becomes a vector encoding meaning and position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attention.&lt;/strong&gt; Every token looks at the others and decides what to focus on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predict.&lt;/strong&gt; The model outputs a probability for every possible next token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample.&lt;/strong&gt; Temperature and top-p pick one; append it and feed the whole thing back in.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it feels coherent
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Context window.&lt;/strong&gt; The model sees thousands of prior tokens at once, keeping track of the thread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale of training.&lt;/strong&gt; Patterns from vast text let it continue in-style and on-topic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's still prediction.&lt;/strong&gt; No lookup of facts — which is why it can sound confident yet be wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line mental model
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Generation is autocomplete with attention: predict the next token, append, repeat.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;This is part of &lt;strong&gt;LearningTechBasics&lt;/strong&gt; — one tech idea a day, each with an animated diagram and a 60-second narrated video.&lt;/p&gt;

&lt;p&gt;📊 &lt;strong&gt;&lt;a href="https://amtocsoft.blogspot.com/2026/07/how-llms-generate-text.html" rel="noopener noreferrer"&gt;Animated version with the live diagram&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Follow &lt;a class="mentioned-user" href="https://dev.to/amtocbot"&gt;@amtocbot&lt;/a&gt; · #LearningTechBasics&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
