<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Tech Unpack: Software]]></title><description><![CDATA[Programming, system design, and software engineering — broken down so anyone curious can follow along.]]></description><link>https://technunpack.substack.com/s/software</link><image><url>https://substackcdn.com/image/fetch/$s_!Cmun!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2db6a7f8-f869-4fd3-99d2-93c7e3134eea_800x800.png</url><title>Tech Unpack: Software</title><link>https://technunpack.substack.com/s/software</link></image><generator>Substack</generator><lastBuildDate>Thu, 07 May 2026 10:24:09 GMT</lastBuildDate><atom:link href="https://technunpack.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Jack Do]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[technunpack@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[technunpack@substack.com]]></itunes:email><itunes:name><![CDATA[Jack Do]]></itunes:name></itunes:owner><itunes:author><![CDATA[Jack Do]]></itunes:author><googleplay:owner><![CDATA[technunpack@substack.com]]></googleplay:owner><googleplay:email><![CDATA[technunpack@substack.com]]></googleplay:email><googleplay:author><![CDATA[Jack Do]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Why Your System Crashes Under Load — and How Kafka and SQS Push Back]]></title><description><![CDATA[Part 3 of "Building Software That Doesn't Fall Over"]]></description><link>https://technunpack.substack.com/p/why-your-system-crashes-under-load</link><guid isPermaLink="false">https://technunpack.substack.com/p/why-your-system-crashes-under-load</guid><dc:creator><![CDATA[Jack Do]]></dc:creator><pubDate>Wed, 06 May 2026 12:09:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/763369a0-82ad-4bfa-85e4-eb4f331e1301_1356x746.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Intro</h3><p>Welcome back. In <a href="https://technunpack.substack.com/p/the-shared-state-problem-how-real">Part 2</a> we looked at what happens when multiple workers fight over the same data &#8212; race conditions, stale reads, and why a single Redis thread is actually a feature. Today we move one layer up the stack.</p><p>The failure mode this time isn&#8217;t a data conflict. It&#8217;s volume. Specifically: what happens when data arrives faster than you can process it, and nothing in your system knows how to push back.</p><p>That&#8217;s backpressure &#8212; or rather, the lack of it.</p><h3>Backpressure in plain terms</h3><p>Every data pipeline has two roles: a <strong>producer</strong> generating work, and a <strong>consumer</strong> processing it. When the producer is faster than the consumer, the difference has to go somewhere &#8212; usually a buffer or queue.</p><p>Backpressure is the mechanism that lets the consumer tell the producer &#8220;slow down, I&#8217;m full.&#8221; Without it, the buffer just grows until something crashes.</p><h3>Three problems, three different solutions</h3><p>In real world backpressure can shows up in different shapes. For this article, I will go through 3 examples with 3 different solutions, hopefully you guys can find actual use case in some of them.</p><h4>Problem 1: Streaming large data through a single process</h4><p><strong>Scenario: </strong> <em>Let&#8217;s imagine a user requests a CSV export of their entire transaction history &#8212; 2 million rows. Your service streams rows from Postgres straight into the HTTP response. In testing with 100 rows, fine. In production, Postgres sends rows faster than the HTTP socket can flush them to the client. Node.js buffers the difference in memory. Within 30 seconds, that single request has consumed 800MB of heap. A few concurrent exports later, the process is killed by the OS.</em></p><p>This is a single-process problem.  Here, all the data is flowing through one Node.js process. The only way out is to slow down the source until the destination catches up.</p><p><strong>The solution: Node.js streams with </strong><code>highWaterMark</code><strong> and </strong><code>.pipe()</code><strong>.</strong></p><p>Every readable and writable stream in Node has a property called <strong>highWaterMark</strong> &#8212; the maximum amount the internal buffer holds before signalling it&#8217;s full. Default is 16KB for byte streams, 16 objects for object-mode streams.</p><p>Here&#8217;s how the signal works. </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;5599632f-7715-4f36-accf-3b372e4469a2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// The easy way &#8212; .pipe() handles backpressure for you
postgresStream.pipe(httpResponse)

// What .pipe() actually does under the hood:
postgresStream.on('data', (chunk) =&gt; {
  // write() returns false when buffer exceeds highWaterMark
  const canContinue = httpResponse.write(chunk)
  
  if (!canContinue) {
    // Buffer is full &#8212; stop reading from Postgres
    postgresStream.pause()
  }
})

// When the buffer drains below highWaterMark, resume reading
httpResponse.on('drain', () =&gt; {
  postgresStream.resume()
})</code></pre></div><p><strong>Rule of thumb</strong>: always use <code>.pipe()</code> instead of <code>writable.write(chunk)</code> and let it handle the loop for you.</p><h4>Problem 2: High-throughput event pipeline between services</h4><p><strong>Scenario:</strong> <em>Your app emits thousands of events per second &#8212; user clicks, payment attempts, feature usage. A downstream analytics service consumes these to update dashboards and write audit trails. During peak hours, events arrive 10x faster than the consumer can process. If nothing slows the producers down, events pile up and crash the consumer, or get dropped on the floor.</em></p><p><strong>The solution:</strong><em> </em>Kafka has a very elegant way to resolve this.</p><p><strong>Quick intro</strong>: <a href="https://www.confluent.io/what-is-apache-kafka/#:~:text=Apache%20Kafka%20is%20an%20open,pipelines%20and%20event%2Ddriven%20applications.">Apache Kafka</a> (or Kafka for short) is a Java based software that is optimized for ingesting and processing high volume of data streaming. </p><p><strong>The key idea</strong>: Kafka stores events in an ordered log called a <strong>topic</strong>. Producers append to the end. Consumers read from wherever they left off, at their own pace. The producer never waits for the consumer &#8212; the log absorbs the difference.</p><p>Each consumer tracks its position with an <strong>offset</strong> &#8212; a number pointing to the last event it processed. The gap between the latest event and the consumer&#8217;s offset is called <strong>consumer lag</strong> &#8212; the early warning signal that the consumer is falling behind.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;9446a54c-6b7b-42d0-9411-c394f6481291&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Fetch a batch of events to process
const analyticsConsumer = kafka.consumer({ groupId: 'analytics' });

await analyticsConsumer.run({
  // max.poll.records: how many events per fetch (default 500)
  // Lower this if processing is slow
  eachBatch: async ({ batch, pause, resume }) =&gt; {
    
    if (downstreamIsOverwhelmed()) {
      // Tell Kafka to stop sending us events
      // Events stay safe in the log &#8212; nothing is lost
      pause([{ topic: 'user-events' }])
      
      // Resume after a cooldown
      setTimeout(() =&gt; resume([{ topic: 'user-events' }]), 30_000)
      return
    }
    
    await processBatch(batch)
    // Commit our new offset &#8212; we've processed up to here
  },
})

const auditConsumer = kafka.consumer({ groupId: 'audit' });
// ....</code></pre></div><p><strong>Key gotcha</strong>: if your processing exceeds <code>max.poll.interval.ms</code> (default 5 minutes), Kafka assumes your consumer is dead and reassigns its work to another consumer. Same events get re-processed when you rejoin. For slow consumers, drop <code>max.poll.records</code> to 10&#8211;50.</p><h4>Problem 3: Massive parallel async tasks</h4><p><strong>Scenario.</strong> <em>You run a serverless image processing pipeline. Users upload images via API, the API drops a message into an SQS queue, and Lambda functions process each image &#8212; resize, watermark, store. Normal traffic is 50 messages/min. A marketing campaign goes live &#8212; suddenly it's 50,000 messages/min. Lambda scales fast and spawns 800 concurrent executions. Each one opens a Postgres connection. Your database has a 100-connection pool. 700 connections get refused, Lambda retries, SQS re-delivers, and you now have a thundering herd pointed straight at your database.</em></p><p>Why doesn't Kafka solve this? Kafka is great for streaming events, but each message here is a <em>task</em> &#8212; one image to process, then delete. You don't need replay, you don't need multiple consumers reading the same data. You need a queue where each task is picked up once, plus an autoscaling consumer that doesn't bury your database.</p><p><strong>The solution</strong>: SQS visibility timeout + Lambda reserved concurrency.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;d947732e-1685-4cca-854b-5e0e179257d7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">const imageJobs = new Queue(this, 'ImageJobs', {
  visibilityTimeout: Duration.seconds(60),
})

const imageProcessor = new Function(this, 'ImageProcessor', {
  runtime: Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: Code.fromAsset('lambda'),
  timeout: Duration.seconds(30),

  // Cap parallel executions &#8212; the actual backpressure mechanism.
  // No matter how many messages pile up in SQS, only 50 Lambdas
  // run at once. Messages wait in the queue for a free slot.
  reservedConcurrentExecutions: 50,
})

imageProcessor.addEventSource(new SqsEventSource(imageJobs, {
  batchSize: 10,
  maxConcurrency: 50,
}))</code></pre></div><p>Without <code>reservedConcurrentExecutions</code>, Lambda will spawn hundreds of parallel functions during a spike &#8212; each opening a database connection, each adding load. The cap is what turns Lambda from "fan out infinitely" into a controlled consumer.</p><h4><strong>Overlaps between AWS SQS/SNS and Apache Kafka</strong></h4><p>From the simple example above, one may wonder we can switch between either SNS/SQS (Topic Fanout design) and Kafka. But in reality, there are huge different between the 2. Here are quick overview of both.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rz1m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rz1m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 424w, https://substackcdn.com/image/fetch/$s_!rz1m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 848w, https://substackcdn.com/image/fetch/$s_!rz1m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 1272w, https://substackcdn.com/image/fetch/$s_!rz1m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rz1m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png" width="1456" height="520" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bef33f83-711e-44b5-be33-1451905fd84d_2336x834.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:520,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:193087,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://technunpack.substack.com/i/196643877?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rz1m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 424w, https://substackcdn.com/image/fetch/$s_!rz1m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 848w, https://substackcdn.com/image/fetch/$s_!rz1m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 1272w, https://substackcdn.com/image/fetch/$s_!rz1m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbef33f83-711e-44b5-be33-1451905fd84d_2336x834.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Conclusion</h3><p>If you guys have reached this point, congrats, it has been a long article that go through briefly 2 very popular technologies in designing distributed system. I will let you guys go for now, and see you all in next part</p><p><em>Next up &#8212; Part 4: Stateless Scaling. How load balancers decide where your request goes, why sticky sessions are a trap, and what "stateless" actually requires from your application layer.</em></p>]]></content:encoded></item><item><title><![CDATA[The Shared State Problem — How real world system actually solve it]]></title><description><![CDATA[Part 2 of "Building Software That Doesn't Fall Over"]]></description><link>https://technunpack.substack.com/p/the-shared-state-problem-how-real</link><guid isPermaLink="false">https://technunpack.substack.com/p/the-shared-state-problem-how-real</guid><dc:creator><![CDATA[Jack Do]]></dc:creator><pubDate>Sun, 03 May 2026 06:47:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b67a7c67-8da0-479e-a12c-eb137b49b3ce_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Intro</h3><p>Welcome back to my deep dive system design series, if you guys read <a href="https://technunpack.substack.com/p/concurrency-vs-parallelism-vs-async">part 1</a> and find them useful, this article will be the following step to answer some of the questions you may have. In previous part, we talked about concurrency, parallelism and async.</p><p>Here's the thing nobody tells you after you understand async: <em>running things concurrently creates a new class of bug</em>. Two requests touch the same balance. A counter drifts. A user gets charged twice. The fix sounds obvious &#8212; "just add a lock" &#8212; until you realize that's the beginning of the problem, not the end.</p><p>In this article I will go into the implementation of 2 famous technologies <strong><a href="https://www.postgresql.org/">Postgres</a></strong> and <strong><a href="https://redis.io/">Redis</a>, </strong>so we can understand how each of them tackle the shared state problem. The third example will be from <strong><a href="https://stripe.com/au">Stripe</a> - </strong>a well known payment processor<strong> </strong>which we will see how &#8216;shared state issues&#8217; got addressed at a distributed system.</p><h3>The technical detour</h3><p>Before deep dive into practical implementations, the following terms are quite common that I think we should get our head around.</p><p><em><strong>Race condition</strong></em> &#8212; is what happens when the correctness of your program depends on the <em>order</em> in which concurrent operations run &#8212; and that order isn&#8217;t guaranteed. It&#8217;s not that the logic is wrong; it&#8217;s that the order has been messed up.</p><p><em><strong>Mutex</strong></em> &#8212; (mutual exclusion lock) is a flag that says &#8220;only one thing at a time can be in this section of code.&#8221; Threads or async tasks line up, take turns, release. Some good examples here are <code>threading.Lock()</code> in Python, or <code>sync.Mutex</code> in Go.</p><p><em><strong>Atomic operation</strong></em> &#8212; is a single instruction the CPU promises to complete without interruption. &#8220;Add 1 to this number&#8221; sounds like one step but is actually three (read, add, write). Atomics make it genuinely one step, so no other thread can sneak in between. Atomic is a fundamental part in ACID transactions property that make engineers choose RDBMS to prioritizes consistency over availability.</p><p><em><strong>Optimistic concurrency</strong></em> &#8212; flips the whole model. Instead of locking before you read, you read freely, do your work, and at write time check &#8220;is the data still what I saw?&#8221; If yes, commit. If no, retry. No queueing, no waiting &#8212; but you pay for it in retries when contention is high.</p><h3>How the smart systems actually handle it</h3><h4><em>Postgres &#8212; versioning instead of locking</em></h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s2Dx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s2Dx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 424w, https://substackcdn.com/image/fetch/$s_!s2Dx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 848w, https://substackcdn.com/image/fetch/$s_!s2Dx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 1272w, https://substackcdn.com/image/fetch/$s_!s2Dx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s2Dx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png" width="1326" height="1088" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1088,&quot;width&quot;:1326,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:206230,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://technunpack.substack.com/i/196276469?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!s2Dx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 424w, https://substackcdn.com/image/fetch/$s_!s2Dx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 848w, https://substackcdn.com/image/fetch/$s_!s2Dx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 1272w, https://substackcdn.com/image/fetch/$s_!s2Dx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F64c6bc70-804c-45a9-9091-9fe7a34377e6_1326x1088.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When txn 101 updates <code>acc_42</code>, Postgres doesn't overwrite the row. It writes a <em>new version</em> of it &#8212; tagged with the transaction ID that created it (<code>xmin=101</code>). The old row stays on disk, tagged as visible to transactions that started before 101. That's why txn 100 &#8212; a long-running analytics query &#8212; still reads <code>$1,000</code> while txn 101 is writing <code>$300</code>. They're reading <em>different physical rows</em>, both legitimate, neither blocking the other. The cost is storage: old row versions accumulate until vacuum reclaims them, which is why long-running transactions are dangerous &#8212; they pin old versions that vacuum can't touch. <em>Deep dive on MVCC, vacuum, and the connection pool problem in Part 5.</em></p><h4><em>Redis &#8212; sidestep the problem entirely</em></h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mCQa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mCQa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 424w, https://substackcdn.com/image/fetch/$s_!mCQa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 848w, https://substackcdn.com/image/fetch/$s_!mCQa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 1272w, https://substackcdn.com/image/fetch/$s_!mCQa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mCQa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png" width="1368" height="1028" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1028,&quot;width&quot;:1368,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:194105,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://technunpack.substack.com/i/196276469?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mCQa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 424w, https://substackcdn.com/image/fetch/$s_!mCQa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 848w, https://substackcdn.com/image/fetch/$s_!mCQa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 1272w, https://substackcdn.com/image/fetch/$s_!mCQa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d822541-2c16-410d-ad0f-158b6180527b_1368x1028.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Redis doesn't solve concurrent shared state &#8212; it makes it structurally impossible. Every command from every client lands in a single queue. The event loop pops one command, runs it to completion, moves to the next. <code>INCR counter</code> is a read, increment, and write &#8212; but because nothing else can run between those steps, it's <em>atomic by design</em>, not by locking. The cost is that one slow command (<code>KEYS *</code>, a blocking <code>LRANGE</code> on a 10M-item list) stalls every other client. Redis is fast precisely because its operations are tiny. If you're running big operations, you're using it wrong. <em>Cache stampedes and why "just put Redis in front of it" has its own failure modes: Part 6.</em></p><h4><em>Stripe &#8212; idempotency keys for distributed shared state</em></h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ghir!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ghir!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 424w, https://substackcdn.com/image/fetch/$s_!ghir!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 848w, https://substackcdn.com/image/fetch/$s_!ghir!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 1272w, https://substackcdn.com/image/fetch/$s_!ghir!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ghir!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png" width="1440" height="1130" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1130,&quot;width&quot;:1440,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:227074,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://technunpack.substack.com/i/196276469?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ghir!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 424w, https://substackcdn.com/image/fetch/$s_!ghir!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 848w, https://substackcdn.com/image/fetch/$s_!ghir!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 1272w, https://substackcdn.com/image/fetch/$s_!ghir!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe886eda-1c25-46f2-9f8e-b11d776c79fd_1440x1130.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Once state lives across machines, in-process locks and MVCC stop helping &#8212; a network retry doesn't care what's in your mutex. Stripe's answer is a lookup table. Before executing anything, the server checks: have I seen this key before? First time &#8212; insert a row, run the charge, store the result, mark complete. Duplicate &#8212; return the stored result, never touch the payment processor. The elegance is in the <code>processing</code> state: if the first request is still in-flight when the retry arrives, the server returns a <code>409 Conflict</code> and the client backs off. The distributed coordination problem reduces to a single database row. <em>Distributed locks, Redlock, and the CAP theorem trade-offs in a later post.</em></p><h3>I know the concepts, what now?</h3><p>The following are some common mistakes that I found even seasoned engineers can make.</p><h4>Mistake 1: Wrapping everything in a giant lock</h4><p>A team hits a race condition, panics, and wraps the entire request handler in a global mutex. Bug fixed. Throughput drops 90%. Lock the smallest unit you can &#8212; a row, a key, a single object. Better yet, ask whether the problem can move to the database where MVCC handles it for you.</p><p><strong>Principle.</strong> <em>Locks are a tax on concurrency.</em> The bigger the lock, the higher the tax. Correct-but-slow under load eventually becomes just wrong, as timeouts cascade into retries into outages.</p><h4>Mistake 2: Forgetting state lives outside your process too</h4><p>A service has perfect in-memory locking. It scales to two replicas. The locks now do nothing &#8212; they only protect within one process. For state shared across machines, you need a coordination layer: a database transaction, a Redis lock, an idempotency key, or a queue with single-consumer semantics.</p><p><strong>Principle.</strong> <em>In-process locks don&#8217;t survive horizontal scaling.</em> The day you add a second replica is the day your single-process locking assumptions quietly break.</p><h4>Decision tree</h4><p>Before reaching for a lock, walk this:</p><ol><li><p><strong>Is the shared state actually shared?</strong> Can each request own its own copy? If yes &#8212; do that, you&#8217;re done.</p></li><li><p><strong>Is the operation a single read-modify-write on one value?</strong> Use an atomic. Done.</p></li><li><p><strong>Are conflicts rare?</strong> Optimistic concurrency (version check, compare-and-swap) &#8212; fast in the happy path.</p></li><li><p><strong>Are conflicts common, but the critical section small?</strong> A scoped mutex on the smallest unit you can.</p></li><li><p><strong>Does the state live across machines?</strong> You&#8217;re not in lock territory anymore &#8212; you&#8217;re in distributed coordination territory. Reach for a database transaction, a queue, or an idempotency key.</p></li></ol><h3><strong>Conclusion</strong></h3><p>And that&#8217;s all what I have for you guys today. Once you&#8217;ve got concurrency working and shared state under control, the next thing to break you is <strong>load itself</strong>. What happens when traffic exceeds capacity? Do requests queue forever? Do you drop them? Do you make the upstream slow down?</p><p>Next post: <strong><a href="https://technunpack.substack.com/p/why-your-system-crashes-under-load">Why Your System Crashes Under Load &#8212; and How Kafka and SQS Push Back</a></strong></p>]]></content:encoded></item><item><title><![CDATA[Concurrency vs Parallelism — Stop Mixing Them Up]]></title><description><![CDATA[Part 1 of "Building Software That Doesn't Fall Over"]]></description><link>https://technunpack.substack.com/p/concurrency-vs-parallelism-vs-async</link><guid isPermaLink="false">https://technunpack.substack.com/p/concurrency-vs-parallelism-vs-async</guid><dc:creator><![CDATA[Jack Do]]></dc:creator><pubDate>Sat, 02 May 2026 01:26:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/55f7e0c3-5446-471f-9177-a0824786a241_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Intro</h3><p>Welcome back to <strong>Tech Unpack</strong>. This is the first deep dive in a seven-part series called <strong>&#8220;Building Software That Doesn&#8217;t Fall Over&#8221;</strong> &#8212; a tour through the engineering decisions that separate systems surviving real production load from systems that buckle the moment traffic spikes.</p><p>A production-grade service has to solve a whole stack of problems: handling huge volumes of concurrent requests, sharing state safely between them, absorbing traffic spikes without crashing, scaling out across machines, surviving database bottlenecks, caching without going stale, and staying observable when things break. Each of these gets its own post in this series.</p><p>Today we&#8217;re starting with the foundation everything else sits on: <strong>how do you serve thousands of concurrent requests on a single machine without it grinding to a halt?</strong></p><p>The answer comes down to picking the right tool for the workload &#8212; and that means getting clear on three concepts that get tossed around interchangeably but actually mean very different things</p><h3>The three words, explained simply</h3><p>Picture a typical workday at the office.</p><ul><li><p><strong>Concurrency</strong> is <em>managing multiple tasks at once</em>. You&#8217;re on a Zoom call, but while someone else is talking, you reply to a Slack message. You&#8217;re not doing two things at the exact same instant &#8212; you&#8217;re juggling them. One person, multiple things in flight.</p></li><li><p><strong>Parallelism</strong> is <em>actually doing multiple tasks at the same instant</em>. You and a colleague each handle your own meeting, at the same time. Two people, two meetings, real wall-clock speedup.</p></li><li><p><strong>Async</strong> is <em>a way to handle concurrent work without blocking</em>. Instead of sitting there refreshing your inbox waiting for a reply, you send the email and move on. When the reply lands, your laptop pings. It&#8217;s a way of working, not a separate concept.</p></li></ul><p><em>The trap most devs fall into: <strong>async code is concurrent, but not parallel.</strong> A single Node.js event loop juggles thousands of async tasks &#8212; but only one runs at a time. If that one task hogs the CPU, everything else waits.</em></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!f59N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!f59N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 424w, https://substackcdn.com/image/fetch/$s_!f59N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 848w, https://substackcdn.com/image/fetch/$s_!f59N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 1272w, https://substackcdn.com/image/fetch/$s_!f59N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!f59N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png" width="1272" height="826" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:826,&quot;width&quot;:1272,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:152005,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://technunpack.substack.com/i/196177307?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!f59N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 424w, https://substackcdn.com/image/fetch/$s_!f59N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 848w, https://substackcdn.com/image/fetch/$s_!f59N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 1272w, https://substackcdn.com/image/fetch/$s_!f59N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1544d21a-af11-4838-bff7-8ba69bac3463_1272x826.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Concepts in action</h3><p>The best way to understand and apply these concepts are by looking at it in a demo. But before going to the fun parts, there are 2 more concepts we need to clear our head. (Yes explain something by giving definitions to something else &#128557;) </p><p><strong>I/O-bound work</strong> is anything where your code is <em>waiting</em> on something external &#8212; a network response, a database query, a file read, an API call. The CPU sits idle during the wait. If you timed the work, most of the milliseconds would be &#8220;doing nothing, waiting for someone else.&#8221;</p><p><strong>CPU-bound work</strong> is anything where your code is <em>actively computing</em> &#8212; hashing, parsing, image resizing, regex matching, JSON-serializing a huge object. The CPU is pegged the whole time. There&#8217;s no waiting; there&#8217;s just work.</p><p>A simple test: <strong>if a faster network would speed up the task, it&#8217;s I/O-bound. If a faster CPU would speed it up, it&#8217;s CPU-bound.</strong></p><h4>Demo: same task, three approaches</h4><p>I built a small TypeScript demo that runs two tasks &#8212; one I/O-bound, one CPU-bound &#8212; three different ways: <strong>sequential</strong>, <strong>async-concurrent (</strong><code>Promise.all</code><strong>)</strong>, and <strong>parallel (worker threads)</strong>. The full code is on GitHub if you want to clone and run it yourself:</p><p>&#128073; <strong><a href="https://github.com/jackdo68/concurrency-demo">github.com/jackdo68/concurrency-demo</a></strong></p><p>So basically there are 2 files in the code base (you already can guess, one for I/O tasks, one for CPU intensive tasks) called <code>io-demo.ts</code> and <code>cpu-demo.ts</code>. </p><p>I/O demo I ran an actual HTTP request to an server using 2 manner <em>sequential</em> and <em>concurrent</em>. And the result as you can tell, the &#8220;concurrent&#8221; will finish first, because while wait for the response of one, the machine can start doing the job of another. The screenshot below shows the result logs in milliseconds</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&gt; concurrency-demo@1.0.0 io
&gt; tsx io-demo.ts

Sequential: 15162ms
Concurrent: 3172ms</code></pre></div><p>CPU demo is about running multiple CPU tensive tasks in 3 manners <em>sequential</em>, <em>promise.all/async</em> and <em>parallel</em> (spawn up child workers). Since the CPU has to actually do the work here, sequential and async manner roughly has the same latency (in fact async has some overhead and actually takes longer) while <em>parallel</em> really shine (but of course with extra help of sub workers/child process)</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&gt; concurrency-demo@1.0.0 cpu
&gt; tsx cpu-demo.ts

Sequential: 4300ms
Promise.all: 4325ms
Parallel: 2236ms</code></pre></div><h3>I know the concepts, what now?</h3><p>Here&#8217;s where the three concepts become a toolkit. Most production outages I&#8217;ve seen come from one of three mistakes &#8212; and each one maps to a misuse of these concepts.</p><h4>Mistake 1: Treating I/O work like CPU work</h4><p><strong>Example</strong> A service makes 50 calls to downstream APIs, one after another. Each takes 200ms. Total: 10 seconds. Under load, requests pile up, the server hits its connection limit, and everything starts 504-ing.</p><p><strong>The fix:</strong> async concurrency. Fire the calls together with <code>Promise.all</code> (or <code>asyncio.gather</code>, or a Go <code>errgroup</code>) and total time drops to ~200ms. Same server, same hardware, 50x throughput on that endpoint &#8212; for free.</p><p><strong>Resilience principle:</strong> <em>Never wait sequentially for things that don&#8217;t depend on each other.</em> This is the cheapest performance win in software, and the easiest one to miss. Companies like Netflix, Uber, and PayPal lean heavily on Node for I/O-heavy services &#8212; API gateways, real-time feeds, backends-for-frontends &#8212; where holding thousands of open connections cheaply matters more than raw compute. <a href="https://nodejs.org/learn/asynchronous-work/dont-block-the-event-loop">Node JS - Event loop</a></p><h4>Mistake 2: Treating CPU work like I/O work</h4><p><strong>Example </strong>A team adds image resizing to their Node API. They wrap it in <code>async/await</code> and assume the event loop will handle it. Under load, every request waits for the resize to finish &#8212; because the event loop is <em>one thread</em>. P99 latency goes through the roof. Health checks start failing because the loop can&#8217;t even respond to <code>/health</code> for 800ms at a time. The load balancer marks the instance unhealthy, traffic shifts to the survivors, and they fall over too.</p><p><strong>The fix:</strong> offload CPU work to a worker pool so the event loop stays free. Or better, push it to a separate service entirely (a queue plus dedicated workers).</p><p><strong>Resilience principle:</strong> <em>CPU work needs its own lane.</em> If a single slow operation can block your health check, your load balancer will eventually kill the instance and your traffic will cascade onto whoever&#8217;s left. Instagram runs a Django monolith on Python, which has the Global Interpreter Lock (GIL) &#8212; only one thread executes Python bytecode at a time &#8212; so they sidestep it with a multiprocess model behind Gunicorn, running many worker processes per machine. <a href="https://instagram-engineering.com/what-powers-instagram-hundreds-of-instances-dozens-of-technologies-adf2e22da2ad">Instagram - Engineering</a></p><h4>Mistake 3: Unbounded concurrency</h4><p><strong>Example </strong>The opposite of mistake 1. A dev hears &#8220;use <code>Promise.all</code>&#8220; and fires 10,000 DB queries at once. The connection pool is exhausted in milliseconds. The DB starts queuing, then timing out. Suddenly <em>every</em> service sharing that DB is degraded &#8212; including the ones that had nothing to do with this request.</p><p><strong>The fix:</strong> bounded concurrency. Use a semaphore or a library like <code>p-limit</code> to cap how many things run at once. Twenty in flight at a time is usually plenty.</p><p><strong>Resilience principle:</strong> <em>Concurrency is a resource &#8212; budget it.</em> &#8220;As fast as possible&#8221; usually means &#8220;fast enough to break something downstream.&#8221; We&#8217;ll go much deeper on this in Part 3 (backpressure).</p><h3>Conclusion</h3><p>Thanks for being patient and keep reading until this point, but this is just only the beginning. Once you have multiple things running at once &#8212; async or parallel &#8212; they start stepping on each other. Two requests update the same counter. A cache write races a cache read. Welcome to <strong>shared state</strong>, the bug factory at the heart of every concurrent system.</p><p>Next post: race conditions, mutexes, and why &#8220;just add a lock&#8221; is the start of a much longer conversation.</p>]]></content:encoded></item></channel></rss>