<?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[Quang Chien's Blog | Software Engineer | France: Technology]]></title><description><![CDATA[Section Technology]]></description><link>https://quangchientran.substack.com/s/technologie</link><image><url>https://substackcdn.com/image/fetch/$s_!LqqI!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa23b0fe9-c371-44b8-a1de-93bf83e76d7a_800x800.png</url><title>Quang Chien&apos;s Blog | Software Engineer | France: Technology</title><link>https://quangchientran.substack.com/s/technologie</link></image><generator>Substack</generator><lastBuildDate>Thu, 14 May 2026 11:03:31 GMT</lastBuildDate><atom:link href="https://quangchientran.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Quang Chien TRAN]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[quangchientran@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[quangchientran@substack.com]]></itunes:email><itunes:name><![CDATA[Quang Chien TRAN]]></itunes:name></itunes:owner><itunes:author><![CDATA[Quang Chien TRAN]]></itunes:author><googleplay:owner><![CDATA[quangchientran@substack.com]]></googleplay:owner><googleplay:email><![CDATA[quangchientran@substack.com]]></googleplay:email><googleplay:author><![CDATA[Quang Chien TRAN]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[EventBridge + Lambda: My Go-To Duo for AWS Automation]]></title><description><![CDATA[From cost optimization and ETL to cron jobs, staging sync, and security remediation.]]></description><link>https://quangchientran.substack.com/p/eventbridge-lambda-aws-automation</link><guid isPermaLink="false">https://quangchientran.substack.com/p/eventbridge-lambda-aws-automation</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Fri, 08 May 2026 20:32:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IMss!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today I want to share a little bit about <strong>AWS</strong> &#8212; a cloud platform that I use quite a lot both at work and in my studies. Not only does it help me deploy systems, but AWS also affects quite a lot the way I think about <strong>architecture design</strong> and <strong>real-world technical solutions</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IMss!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IMss!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 424w, https://substackcdn.com/image/fetch/$s_!IMss!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 848w, https://substackcdn.com/image/fetch/$s_!IMss!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 1272w, https://substackcdn.com/image/fetch/$s_!IMss!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IMss!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png" width="1456" height="582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:582,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1122430,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/196934594?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!IMss!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 424w, https://substackcdn.com/image/fetch/$s_!IMss!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 848w, https://substackcdn.com/image/fetch/$s_!IMss!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 1272w, https://substackcdn.com/image/fetch/$s_!IMss!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8594ca61-86bb-4bc6-b6b2-f434068ac377_1983x793.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"></div></div></a></figure></div><p>Before, I had written a post about how to <strong>optimize AWS infrastructure costs</strong> in a company, with some ways of thinking that helped reduce the <strong><a href="https://open.substack.com/pub/quangchientran/p/3-how-i-reduced-aws-costs-by-50?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">cloud bill by around 40%</a></strong> while the system still ran stably. In this post, I&#8217;ll talk about a <strong>&#8220;duo&#8221;</strong> that I use very often for automation: <strong>EventBridge and Lambda</strong>. These two services work together in a pretty lightweight way, but they solve a lot of <strong>real-world problems</strong>.</p><div><hr></div><h2><strong>0. Definition</strong></h2><h3><strong>What is Lambda?</strong></h3><p>Simply put, <strong>AWS Lambda</strong> is a service that lets you run code <strong>without managing servers</strong>. You only need to write the code to handle <strong>business logic</strong>, and AWS will take care of the rest such as <strong>provisioning</strong>, <strong>scaling</strong>, and <strong>infrastructure operations</strong>.</p><p>However, &#8220;not managing servers&#8221; does not mean you don&#8217;t need to care about anything. In practice, you still need to configure a few things such as <strong>memory</strong>, <strong>timeout</strong>, <strong>IAM permissions</strong>, or optimize to reduce <strong>cold start</strong>.</p><p>Usually, you write code as a <strong>function</strong> (the entry point is called a <strong>handler</strong>), and Lambda will execute that function when an <strong>event</strong> is triggered. Lambda supports many runtimes such as <strong>Node.js</strong>, <strong>Python</strong>, <strong>Java</strong>&#8230;, and for me, <strong>Node.js</strong> is the preferred choice because I&#8217;m most comfortable with it and it has quite a fast cold start in many cases.</p><p>Lambda has a lot of use cases, but in this post I&#8217;ll focus only on <strong>automation</strong> combined with <strong>EventBridge</strong>.</p><h3><strong>What is EventBridge?</strong></h3><p><strong>EventBridge</strong> is an <strong>event processing</strong> service in AWS. It allows you to receive events from many different sources such as <strong>AWS services</strong>, <strong>CloudTrail</strong>, or <strong>custom events</strong> that you send yourself.</p><p>One important point is that EventBridge does not directly &#8220;interfere&#8221; with the system. Instead, it works as an <strong>event router</strong>: when an event happens, you define <strong>rules</strong> to catch that event and trigger the corresponding <strong>targets</strong> (for example <strong>Lambda</strong>, <strong>SQS</strong>, <strong>Step Functions</strong>&#8230;).</p><p>In this post, I&#8217;ll focus only on EventBridge&#8217;s <strong>Schedule</strong> feature &#8212; that is, running jobs on a time basis (<strong>cron</strong> or <strong>rate</strong>). This is a very convenient way to build <strong>automated periodic tasks</strong> without having to set up a separate server.</p><h3><strong>Why do I often use this duo?</strong></h3><p>For me, <strong>EventBridge + Lambda</strong> is a very <strong>lightweight</strong> combo for automation:</p><ul><li><p>No need to build a separate <strong>cron server</strong>.</p></li><li><p><strong>Easy to scale</strong>.</p></li><li><p><strong>Low cost</strong> if the workload is not large.</p></li><li><p>Built right into the <strong>AWS ecosystem</strong>.</p></li></ul><p>In many real-world cases, just one <strong>schedule rule</strong> + one <strong>Lambda function</strong> is enough to solve an entire <strong>operational problem</strong>.</p><div><hr></div><h2><strong>1. Infrastructure cost management and optimization</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!I7LG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!I7LG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 424w, https://substackcdn.com/image/fetch/$s_!I7LG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 848w, https://substackcdn.com/image/fetch/$s_!I7LG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 1272w, https://substackcdn.com/image/fetch/$s_!I7LG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!I7LG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png" width="1456" height="799" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:799,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1032341,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/196934594?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!I7LG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 424w, https://substackcdn.com/image/fetch/$s_!I7LG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 848w, https://substackcdn.com/image/fetch/$s_!I7LG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 1272w, https://substackcdn.com/image/fetch/$s_!I7LG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb88377a4-f24e-46e4-abf7-fddb84cbde6e_1693x929.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>This is probably one of the most common use cases in Cloud work, especially if you are running multiple environments (<strong>dev</strong>, <strong>staging</strong>, <strong>sandbox</strong>&#8230;). Doing this well not only saves quite a bit of money, but also helps the operations team not have to &#8220;watch the budget&#8221; every end of month.</p><p>One of the ways I use the most is <strong><a href="https://open.substack.com/pub/quangchientran/p/3-how-i-reduced-aws-costs-by-50?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">automatically turning resources on and off by time slot</a></strong>. Specifically, I use <strong>EventBridge Scheduler</strong> to trigger Lambda on a schedule (<strong>cron</strong>), for example at <strong>6 PM</strong>, when everyone is done for the day, turn off all unnecessary resources, then turn them back on at <strong>8 AM</strong> the next morning.</p><p>This is mainly applied to <strong>Development</strong> or <strong>Staging</strong> environments. Resources suitable for this approach:</p><ul><li><p><strong>EC2</strong> &#8594; the easiest, <strong>stop/start directly</strong>.</p></li><li><p><strong>RDS</strong> &#8594; can be <strong>stopped/started</strong>, but you need to note that AWS only allows stopping for a maximum of <strong>7 days</strong>, after which it will start back up automatically.</p></li><li><p><strong>ASG</strong> &#8594; there is no concept of &#8220;turning it off&#8221;, instead you set <strong>desired capacity to 0</strong> (or scale down on schedule).</p></li><li><p><strong>ECS</strong> &#8594; usually I will:</p><ul><li><p>set the service&#8217;s <strong>desired count to 0</strong> to &#8220;turn it off&#8221;.</p></li><li><p>when turning it back on, <strong>scale it up again</strong> as before.</p></li><li><p>if using <strong>Fargate</strong>, this approach is very effective because <strong>scale = 0</strong> means almost no compute cost.</p></li></ul></li><li><p><strong>EKS</strong> &#8594; waiting for you guys to add more here, for now I&#8217;m only working with <strong>ECS</strong>.</p></li></ul><p>Besides turning resources on and off by schedule, another problem that is very often forgotten is cleaning up <strong>&#8220;trash&#8221; resources</strong>. I usually set up a job that runs periodically (for example daily or weekly) to:</p><ul><li><p>Scan <strong>EBS Volumes</strong> that are no longer attached to EC2.</p></li><li><p>Delete <strong>snapshots</strong> that are too old (for example <strong>&gt; 30 days</strong>).</p></li><li><p>Clean up <strong>Elastic IPs</strong> that are no longer attached to any resource.</p></li></ul><p>It sounds simple, but if you do this carelessly, it can blow up very easily &#128517;. My experience is always to filter by <strong>tag</strong> (e.g. <code>env=dev</code>, <code>auto-clean=true</code>), or by clear rules (<strong>age</strong>, <strong>owner</strong>, <strong>project</strong>), and avoid deleting these resources just based on status alone. Because in reality, there are many resources that look like they are &#8220;not in use&#8221; but:</p><ul><li><p>are waiting to be attached,</p></li><li><p>are backups for rollback,</p></li><li><p>or belong to <strong>compliance</strong> processes.</p></li></ul><p>The whole flow is usually:</p><blockquote><p><strong>EventBridge (schedule) &#8594; Lambda &#8594; AWS SDK (scan + action)</strong></p></blockquote><p>Once set up, it runs almost completely automatically, saving cost and reducing quite a lot of manual work for the operations team.</p><div><hr></div><h2><strong>2. Data and file processing</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vUe8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vUe8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 424w, https://substackcdn.com/image/fetch/$s_!vUe8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 848w, https://substackcdn.com/image/fetch/$s_!vUe8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 1272w, https://substackcdn.com/image/fetch/$s_!vUe8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vUe8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png" width="1456" height="728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:728,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1105170,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/196934594?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!vUe8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 424w, https://substackcdn.com/image/fetch/$s_!vUe8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 848w, https://substackcdn.com/image/fetch/$s_!vUe8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 1272w, https://substackcdn.com/image/fetch/$s_!vUe8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c25c9-8452-48e6-a0bd-be429707bc3a_1774x887.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>In practice, I quite often run into problems involving data from third parties. Usually, they provide a pretty large data file (<strong>CSV</strong>, <strong>JSON dump</strong>&#8230;), but on my side I only need a small portion of the data inside it for business purposes.</p><p>If you load the entire file and process it directly on the server, it both wastes resources and is not optimal &#8212; especially when that data is updated daily. At that point, I need to make sure of two things: processing is fast enough and the data is always the latest version.</p><p>The way I usually do it is by setting up a simple <strong>ETL</strong> flow with <strong>EventBridge</strong> and <strong>Lambda</strong>. Specifically, I use <strong>EventBridge Scheduler</strong> to run on a schedule (for example once per day). Then it triggers Lambda to perform <strong>ETL</strong> (<strong>Extract &#8211; Transform &#8211; Load</strong>).</p><p>Inside Lambda, I will:</p><ul><li><p><strong>Extract:</strong> get data from the source (<strong>API</strong> or file).</p></li><li><p>If it is a large file, I prefer to process it in a <strong>streaming</strong> way or split it up instead of loading everything into memory.</p></li><li><p><strong>Transform:</strong> filter out exactly the data I need, normalize the format if necessary.</p></li><li><p><strong>Load:</strong> write the processed data into <strong>S3</strong> (as a <strong>data lake</strong>) or into a database such as <strong>RDS / DynamoDB</strong> to serve other services.</p></li></ul><p>This approach helps reduce the load on the main services (no heavy data processing at runtime). The data is always prepared in advance, so you just query and use it right away, and it is easy to scale and almost does not require server maintenance.</p><p>However, Lambda is not always the right choice. For <strong>small or medium ETL jobs</strong> (data not too large, processed within a few minutes), Lambda works very well. But if the file is too large (several <strong>GB</strong> or more) or the transformation logic is complicated, then you should consider moving to <strong>AWS Glue</strong> (specialized ETL) or an <strong>ECS/Fargate job</strong> to process in batch.</p><p>One point I find very important but easy to overlook is the <strong>&#8220;safety&#8221; of the pipeline</strong>:</p><ul><li><p>There should be <strong>retry</strong> or <strong>DLQ</strong> if the job fails.</p></li><li><p>Avoid blindly <strong>overwriting data</strong> (you can partition by date or version).</p></li><li><p>Make sure the job can run again without producing duplicate data (<strong>idempotency</strong>).</p></li></ul><p>Overall, for simple to medium ETL problems, the combo of <strong>EventBridge + Lambda + S3/DB</strong> is a very neat solution, easy to deploy, and the cost is also quite reasonable.</p><div><hr></div><h2><strong>3. Cron Job</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QhnS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QhnS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 424w, https://substackcdn.com/image/fetch/$s_!QhnS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 848w, https://substackcdn.com/image/fetch/$s_!QhnS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 1272w, https://substackcdn.com/image/fetch/$s_!QhnS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QhnS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png" width="1456" height="799" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:799,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1136386,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/196934594?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!QhnS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 424w, https://substackcdn.com/image/fetch/$s_!QhnS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 848w, https://substackcdn.com/image/fetch/$s_!QhnS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 1272w, https://substackcdn.com/image/fetch/$s_!QhnS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cfe70ad-f954-4cb2-8544-8797f0538fe1_1693x929.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p><strong>Cron jobs</strong> are one of the things that almost every company has to use, especially for scheduled tasks such as <strong>batch processing</strong>, <strong>data synchronization</strong>, <strong>periodic reports</strong>, or performing <strong>billing steps</strong> on a monthly subscription cycle for customers.</p><p>Before, I also used scheduling directly inside Spring Boot applications with <code>@Scheduled</code>. That works, but when running in an environment with multiple <strong>instances</strong>, <strong>pods</strong>, or <strong>containers</strong>, problems start to appear. If there is no mechanism to prevent duplicate execution, the same job can easily be triggered multiple times by different instances.</p><p>Besides that, debugging and operations are also quite inconvenient. A job running inside the app means logs, retries, timeout, or failure are all tightly coupled to the application runtime, so when you need to scale or separate responsibilities, it no longer feels that smooth.</p><p>So for scheduled jobs, I usually separate scheduling from the main application. Specifically, I let <strong>EventBridge</strong> handle the schedule, and then <strong>Lambda</strong> becomes the place where the job is executed.</p><p>This flow is pretty neat:</p><ul><li><p><strong>EventBridge</strong> runs on the configured schedule.</p></li><li><p><strong>Lambda</strong> is invoked to perform a specific task.</p></li><li><p>Lambda can call the application&#8217;s API behind an <strong>ALB</strong>, or directly process the required logic.</p></li></ul><p>If the API is protected by <strong>OAuth</strong>, that&#8217;s even better, because Lambda can act as an <strong>internal client</strong>, get a token, and call the endpoint securely. This approach has a few points that I find very valuable:</p><ul><li><p>No need to embed <strong>cron logic</strong> into the main application.</p></li><li><p>Easier to scale because the <strong>schedule</strong> is separated from the app runtime.</p></li><li><p>Reduces the risk of <strong>duplicate job execution</strong> when the system has multiple instances.</p></li><li><p>Operations and monitoring are also clearer.</p></li></ul><p>In short, for periodic jobs that require high stability, I think letting <strong>EventBridge</strong> handle scheduling and <strong>Lambda</strong> handle execution is a pretty clean, neat, and maintainable direction.</p><div><hr></div><h2><strong>4. Syncing the staging database from production</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!D-W0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!D-W0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!D-W0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!D-W0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!D-W0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!D-W0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1335123,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/196934594?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!D-W0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!D-W0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!D-W0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!D-W0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54bdbd6a-fad5-42f5-af57-5d54de8e6c6c_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>Recently I had a pretty practical task: how to make the <strong>staging database</strong> refresh from <strong>production</strong> every day, so that the team can <strong>debug</strong>, <strong>test</strong>, and handle incidents on data that is as close to real-world as possible.</p><p>This problem sounds simple, but in reality it has a few quite <strong>&#8220;demanding&#8221;</strong> requirements:</p><ul><li><p>The staging data must be updated daily.</p></li><li><p>The staging database endpoint must stay fixed.</p></li><li><p>The staging user and password must remain the same so the dev team does not have to keep changing their connections.</p></li><li><p>The refresh process must be automatic, safe, and require as little manual work as possible.</p></li></ul><p>For this problem, I chose to do it with <strong>EventBridge + Lambda</strong>.</p><p>The first thing I needed to solve was a <strong>fixed endpoint</strong>. Because every time you restore from a snapshot, AWS creates a new <strong>RDS instance</strong> with a new endpoint, so I do not let the app connect directly to the real RDS endpoint. Instead, I create a <strong>CNAME record</strong> in Route 53 to act as the fixed endpoint for staging. The app only needs to point to this hostname, and behind it, it will redirect to the newest staging instance.</p><p>I split the flow into two steps.</p><p><strong>Flow 1: Restore new staging</strong></p><p>On day N, Lambda will restore the latest snapshot from production to create a brand-new staging RDS instance. This instance will become the staging version for that day, with data refreshed from the latest production state.</p><p><strong>Flow 2: Normalize and redirect</strong></p><p>After about <strong>30 minutes</strong>, when the new instance has finished restoring and moved to the <code>available</code> state, Lambda will be triggered again and do 3 things:</p><ul><li><p>Reset the staging database password to a predefined value.</p></li><li><p>Update the CNAME in Route 53 to point to the endpoint of the newly restored instance.</p></li><li><p>Delete the staging database from day N-1 to avoid unnecessary cost.</p></li></ul><p>All required information such as the password, the current staging database identifier, and the old staging database identifier are centrally managed in <strong>Secrets Manager</strong> and <strong>Parameter Store</strong>. This makes operations safer and also easier to trace when you need to track the state of the system.</p><p>Finally, the whole workflow is scheduled by <strong>EventBridge</strong> at the time I choose, so there is almost no manual intervention needed every day.</p><div><hr></div><h2><strong>5. Security and compliance</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7dzw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7dzw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 424w, https://substackcdn.com/image/fetch/$s_!7dzw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 848w, https://substackcdn.com/image/fetch/$s_!7dzw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 1272w, https://substackcdn.com/image/fetch/$s_!7dzw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7dzw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png" width="1456" height="757" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:757,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1189463,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/196934594?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!7dzw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 424w, https://substackcdn.com/image/fetch/$s_!7dzw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 848w, https://substackcdn.com/image/fetch/$s_!7dzw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 1272w, https://substackcdn.com/image/fetch/$s_!7dzw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b56c503-4ca6-44de-a327-ef1492641a02_1739x904.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>One of the big values of automation on AWS is helping the business respond faster to <strong>security risks</strong>, instead of waiting for operators to detect and handle them manually. Some situations I often think about are:</p><ul><li><p>A new <strong>IAM user</strong> is created but not according to the process.</p></li><li><p>An <strong>IAM role</strong> is granted overly broad permissions, such as <strong>AdministratorAccess</strong>.</p></li><li><p>An <strong>S3 bucket</strong> is switched to public mode by mistake.</p></li></ul><p>For these cases, I can use <strong>EventBridge</strong> to receive security events recorded through <strong>CloudTrail</strong>, then trigger <strong>Lambda</strong> to handle them automatically according to pre-defined rules. The flow is usually:</p><ul><li><p><strong>CloudTrail</strong> records the change event.</p></li><li><p><strong>EventBridge</strong> catches that event and triggers <strong>Lambda</strong>.</p></li><li><p>Lambda performs <strong>remediation</strong>, for example revoking permissions, returning the configuration to a safe state, or sending alerts to the operations team.</p></li></ul><p>In many cases, I do not necessarily auto-fix everything immediately. There are situations where it is better to first alert via <strong>Slack</strong>, email, or <strong>SMS</strong> so the team can confirm, especially when an automatic fix could affect a system that is currently running.</p><p>This approach helps security and CloudOps teams respond faster, reduces exposure time to risk, and keeps the system aligned with security policy more effectively.</p><div><hr></div><h2><strong>6. Extra use cases</strong></h2><p>Besides the cases I&#8217;ve already mentioned above, <strong>EventBridge + Lambda</strong> also has quite a few small but very practical uses in system operations. This is the kind of automation that is not too &#8220;grand,&#8221; but it reduces a lot of manual work for the team.</p><p>Some cases I find quite useful:</p><ul><li><p><strong>Automatically sending periodic reports</strong><br>For example, every morning Lambda runs to aggregate metrics from a database, S3, or an internal API, then sends the report through email or Slack to the team.</p></li><li><p><strong>Scheduled health checks</strong><br>EventBridge triggers Lambda to call important APIs / endpoints. If the endpoint has a problem, an alert is sent immediately to the operations team.</p></li><li><p><strong>Cleaning up temporary data and old artifacts</strong><br>It can be used to delete temp files, old logs, old backups, or unnecessary artifacts to reduce storage cost.</p></li><li><p><strong>Handling lightweight reconcile jobs</strong><br>For example, syncing status between systems, checking data mismatches, or updating records that are in the wrong state.</p></li><li><p><strong>Automatically checking and reminding secret rotation</strong><br>If a secret or API key is about to expire, Lambda can send a warning so the team can handle it before it affects production.</p></li><li><p><strong>Orchestrating small scheduled tasks</strong><br>For simple workflows, I can let EventBridge trigger Lambda step by step instead of building a heavier workflow engine.</p></li></ul><p>What I like about this combo is that it is very neat. No need to build another server, no need to maintain cron on a separate machine, and yet it still solves a lot of real-world operational problems.</p><p>If a use case starts becoming more complex, with more branches, or needs clearer orchestration, then that is when I would consider moving to <strong>Step Functions</strong> or another workflow solution.</p><div><hr></div><h2><strong>Conclusion</strong></h2><p>Overall, I think <strong>EventBridge + Lambda</strong> is a very worthwhile duo if you want to automate operational tasks, data processing, cron jobs, or even some simple security flows in AWS. Its strengths are that it is neat, requires little infrastructure management, is easy to scale, and fits a lot of real-world problems.</p><p>Of course, not every case should use Lambda. If a job is too heavy, runs too long, or the workflow is too complex, I would consider moving to <strong>Glue</strong>, <strong>ECS</strong>, <strong>Step Functions</strong>, or another more suitable solution. But for small and medium problems, especially scheduled tasks or event-triggered tasks, this combo is really worth it.</p><p>I wrote this post not to say this is the only right way, but rather one way I have used quite a lot in practice and found effective. If you guys have other good ways, more optimized approaches, or real-world experience with EventBridge and Lambda, I&#8217;d really love to learn more.</p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Node.js and Java: 5 Easy-to-Mix-Up Questions About Runtime, Event Loop, and I/O]]></title><description><![CDATA[How are Node.js and Java different? This article analyzes 5 real-world questions about event loop, Cluster, cold start, WebFlux, Virtual Threads, and frontend/backend runtimes.]]></description><link>https://quangchientran.substack.com/p/nodejs-vs-java-runtime-event-loop-virtual-threads</link><guid isPermaLink="false">https://quangchientran.substack.com/p/nodejs-vs-java-runtime-event-loop-virtual-threads</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Sun, 03 May 2026 15:18:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6qdN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Continuing from my earlier posts about <strong><a href="https://open.substack.com/pub/quangchientran/p/under-the-hood-a-deep-dive-into-processes?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Process</a></strong><a href="https://open.substack.com/pub/quangchientran/p/under-the-hood-a-deep-dive-into-processes?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">, </a><strong><a href="https://open.substack.com/pub/quangchientran/p/under-the-hood-a-deep-dive-into-processes?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Thread</a></strong>, and <strong><a href="https://open.substack.com/pub/quangchientran/p/java-virtual-threads-the-end-of-the?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Virtual Threads</a></strong> in Java, I&#8217;ve come to appreciate how powerful Java has become, especially since <strong>Java 21</strong>. If we look back more than 10 years ago, Java was almost the default choice for many backend systems. But over the last few years, the landscape has changed quite a bit, and a number of new languages and runtimes have emerged that make development faster, lighter, and more flexible.</p><p>One of the most prominent names is <strong>Node.js</strong> &#8212; the JavaScript runtime. Even today, Node.js remains one of the most popular choices for backend development, especially when you need strong I/O performance, fast startup, or want to leverage the same JavaScript ecosystem from frontend to backend. And on the frontend side, JavaScript is still basically the king.</p><p>This article is not meant to introduce Node.js from scratch. Instead, it focuses on the questions I think many of us have asked at some point: How does Node.js actually work? What is it good at? Where does it struggle? And what are the mechanisms hidden underneath those things that seem so simple at first glance? Let&#8217;s get started.</p><div><hr></div><h2><strong>1. How can Node.js handle millions of concurrent requests?</strong></h2><p>The short answer is: <strong>it depends</strong>.</p><p>Node.js can handle a very large number of concurrent requests extremely well, but that is mainly true when your workload is mostly <strong>I/O-bound</strong> &#8212; meaning the system spends most of its time waiting rather than computing. Typical examples include:</p><ul><li><p>Waiting for the database to return results.</p></li><li><p>Waiting to read or write files.</p></li><li><p>Waiting to call a third-party API.</p></li><li><p>Waiting for network responses.</p></li></ul><p>On the other hand, if your application is mostly <strong>CPU-bound</strong> &#8212; meaning it has to do heavy computation, video processing, data compression, encryption, or complex algorithms &#8212; then Node.js is not the ideal choice if you let everything run directly on the event loop.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6qdN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6qdN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!6qdN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!6qdN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!6qdN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6qdN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1424218,&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://quangchientran.substack.com/i/196315396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.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_!6qdN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!6qdN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!6qdN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!6qdN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c35fc54-eed6-410a-9818-afd8154b43fc_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3><strong>Single Thread and Event Loop</strong></h3><p>Most traditional frameworks, such as Java-based ones, use a <strong>multi-threaded model</strong>. In other words, when a request comes in, the framework creates a separate thread to handle that request. <strong>If you have one million requests, that could mean one million threads at the same time</strong>, which quickly leads to massive memory usage and eventually RAM exhaustion. On top of that, the <strong>cost of <a href="https://quangchientran.substack.com/i/194205408/the-cost-of-switching">context switching</a> between those threads can make the application slower instead of doing useful work</strong>.</p><p>By default, a Node.js process runs JavaScript on a <strong>main thread</strong>, and that is what makes the event loop so efficient for I/O-bound workloads. At the same time, Node.js still has internal mechanisms and can be scaled across multiple cores when needed.</p><p>That <strong>single main thread handles incoming work without the overhead of creating a new thread for every request</strong>, so memory usage and context switching costs are much lower. The event loop keeps moving tasks into that main thread for execution. Even if your server has many CPU cores, a single Node.js process still primarily runs JavaScript on one main thread.</p><h3><strong>Non-Blocking I/O</strong></h3><p>Most of a web server&#8217;s time is spent waiting: waiting for a DB result, waiting for file reads, waiting for third-party APIs. With <strong>Java before version 21, that usually meant the thread would sit there and block, doing nothing until the result returned</strong>.</p><p>With Node.js, when an I/O request is made, the <strong>event loop forwards that work down to the runtime&#8217;s async I/O layer</strong> instead of running it directly on the JavaScript thread.</p><p>For operations like file access, network requests, or database calls, Node.js relies on the runtime&#8217;s asynchronous I/O mechanisms and the kernel. On Linux, this is commonly associated with <strong>epoll</strong>; on macOS, <strong>kqueue</strong>; and on Windows, <strong>IOCP</strong>. These mechanisms let Node register interest in file descriptors or sockets, then wait for the kernel to signal when they&#8217;re ready instead of blocking the JavaScript thread.</p><p>The important thing is this: the OS does not magically &#8220;<strong><s>push data directly into the event loop</s></strong>.&#8221; In reality, Node/libuv registers interest in the I/O event, the kernel tracks the I/O state, and once the socket or file is ready, the kernel notifies the event loop that it can continue processing.</p><h3><strong>How does the OS notify Node.js?</strong></h3><p>A more accurate way to describe it is:</p><ul><li><p>Node/libuv submits the I/O request to the operating system layer.</p></li><li><p>The operating system tracks that I/O state.</p></li><li><p>When the I/O completes or data becomes available, the kernel returns a &#8220;ready&#8221; signal.</p></li><li><p>The event loop receives that signal and pulls the corresponding callback or continuation from the queue to run on the main thread.</p></li></ul><p>So it&#8217;s not that the file or database directly talks to JavaScript. It&#8217;s the kernel plus the event notification mechanism informing the runtime that the resource is ready.</p><p>A good way to picture this is with <strong>Promise</strong>, <code>async</code>, and <code>await</code>. When a Promise is resolved, the code waiting on <code>await</code> does not start executing immediately in the middle of the event loop. It is usually placed into the Promise queue or microtask queue, and it gets priority after the current callback finishes, before the event loop moves to the next phase.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;42ec341c-f4cf-42d4-8f2c-018c9cea36ef&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">async function demo() {
  console.log("A")
  const data = await fetch("https://example.com")
  console.log("B", data)
}</code></pre></div><p>What really happens is:</p><ul><li><p>Print <code>A</code></p></li><li><p>Send the network request down to the async I/O layer</p></li><li><p><code>demo()</code> pauses at <code>await</code></p></li><li><p>The event loop continues doing other work</p></li><li><p>When the response comes back, the Promise is resolved</p></li><li><p><code>console.log("B", data)</code> is queued to run next</p></li></ul><p>Think of it like a restaurant with only one waiter. After the waiter takes an order, he sends it to the kitchen to be prepared. While the kitchen is working, he doesn&#8217;t stand there waiting for just one table. Instead, he goes to take more orders or serve other tables. Once the food is ready, the kitchen rings a bell and places the meal in the pickup area. One waiter like that can serve hundreds of tables if he is fast enough.</p><h3><strong>The bottleneck problem</strong></h3><p>Because Node.js has only one main thread, if that thread is busy doing a computation that takes 5 seconds, then during those 5 seconds:</p><ul><li><p>It cannot accept new requests.</p></li><li><p>It cannot respond to tasks that have already been forwarded to the OS and are now ready to return to the event loop.</p></li><li><p>The whole application can appear to freeze.</p></li></ul><p>In contrast, multi-threaded languages like Go or Java can move that work onto another thread on another CPU core, allowing the main thread to keep accepting requests.</p><div><hr></div><h2><strong>2. Can Node Cluster solve CPU-bound problems?</strong></h2><p>By default, Node.js runs JavaScript on a single main thread per process, so a single process cannot automatically use all CPU cores. If your server has 10 cores, then the other 9 cores will mostly sit idle.</p><p>The <strong>Cluster</strong> module is a built-in feature that allows you to create multiple worker processes running in parallel. These processes share the same network port, which makes it possible to distribute the load across multiple CPU cores.</p><ul><li><p><strong>Master Process</strong>: acts as the manager, monitoring and coordinating workers.</p></li><li><p><strong>Worker Process</strong>: individual copies of the application that handle requests directly.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PPCM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PPCM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!PPCM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!PPCM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!PPCM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PPCM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1701355,&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://quangchientran.substack.com/i/196315396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.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_!PPCM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!PPCM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!PPCM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!PPCM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3975ff13-ad4d-43ad-94b6-6a589a4becf7_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3><strong>What problem does Cluster solve?</strong></h3><p>Instead of wasting resources, Cluster lets you run a number of processes roughly equal to the number of CPU cores. Overall system throughput can increase significantly.</p><p>If one worker crashes because of a bug, the other workers can keep serving traffic. The master process can also be configured to automatically respawn a new worker to replace it.</p><p>When there are multiple workers, if one worker is busy struggling with a heavy CPU-bound task, the operating system and Cluster master can route new requests to other workers that are still free.</p><p>Important note: Cluster does <strong>not</strong> make the heavy computation itself faster, but it prevents one expensive task from bringing down the entire server.</p><h3><strong>Limitations</strong></h3><p>Even though Cluster is powerful, it still has some important limitations:</p><ul><li><p><strong>No shared memory</strong>: each worker is a separate process with its own memory space. You cannot store a global variable in Worker A and expect Worker B to read it. To share data, you need external tools such as Redis or a database.</p></li><li><p><strong>More complex management</strong>: session management becomes harder because a user&#8217;s requests may land on different workers. This is usually handled with sticky sessions or a centralized session store.</p></li></ul><h3><strong>Alternative: Worker Threads</strong></h3><p>If Cluster creates multiple independent processes, then <strong>Worker Threads</strong> &#8212; introduced in Node.js v10.5.0 &#8212; let you create multiple threads within the same process.</p><ul><li><p><strong>Cluster</strong> is best for scaling HTTP servers and I/O-bound workloads.</p></li><li><p><strong>Worker Threads</strong> are better for heavy CPU-bound tasks inside a single instance because they can share memory through <code>SharedArrayBuffer</code>, which makes data exchange very fast.</p></li></ul><p>So if you want your Node.js application to perform CPU-bound tasks without blocking the server, you should either use Worker Threads or move that work into a separate service written in a language that is stronger for heavy computation.</p><div><hr></div><h2><strong>3. Why does Node.js start faster than Java?</strong></h2><p>From a practical point of view, Node.js usually starts faster than Java, especially compared to Java applications that use heavyweight frameworks like Spring Boot. That does not mean Java is &#8220;slow.&#8221; It simply means Java and Node.js have different startup models, different initialization costs, and different design philosophies.</p><p>A simple analogy:</p><ul><li><p>Node.js is like a motorcycle: you turn it on and it&#8217;s ready to go.</p></li><li><p>Java is like a bus: before it starts moving, it needs to go through several preparation steps.</p></li></ul><p>This analogy is not meant to say one technology is absolutely better than the other. It just highlights that Node.js usually has a lower startup latency.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GYNh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GYNh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!GYNh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!GYNh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!GYNh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GYNh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1402732,&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://quangchientran.substack.com/i/196315396?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.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_!GYNh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!GYNh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!GYNh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!GYNh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffedeb1ce-3edd-4d22-b1ad-d73b5f8de6bc_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3><strong>Interpreted vs Compiled</strong></h3><p>Node.js runs JavaScript on the <strong>V8 engine</strong>. V8 does more than just &#8220;read code and run it.&#8221; It uses <strong>JIT (Just-In-Time Compilation)</strong>. When the application starts, V8 can parse and execute code very early, then gradually optimize the parts that are used frequently.</p><p>In other words, Node.js does not need to prepare too much before handling the first request. It can start working quickly and optimize over time.</p><p>Java, on the other hand, runs on the <strong>JVM</strong>, and the JVM typically has to do more work during startup:</p><ul><li><p>Load classes into memory.</p></li><li><p>Verify bytecode.</p></li><li><p>Link required components.</p></li><li><p>Initialize runtime structures such as heap, stack, metaspace, and other internal components.</p></li></ul><p>These steps give Java a very strong foundation for long-running systems, but they also make startup slower than Node.js in many cases.</p><h3><strong>Why does Spring Boot start slower?</strong></h3><p>When comparing Express in Node.js with Spring Boot in Java, the difference becomes even more obvious:</p><ul><li><p><strong>Node.js</strong> follows a minimalistic philosophy. You require only what you need. Things remain relatively lightweight and isolated, and unused components do not need to be initialized early.</p></li><li><p><strong>Java</strong>, especially with frameworks like Spring Boot, often uses annotation scanning and dependency injection. During startup, it has to scan the project for things like <code>@Service</code>, <code>@Controller</code>, and <code>@Component</code>, build the dependency injection container, create and wire beans, apply auto-configuration, and set up multiple layers of framework abstraction.</p></li></ul><p>That is why Spring Boot startup tends to feel heavier. But this is not a flaw &#8212; it is the cost of a powerful and convenient enterprise framework. Java was built for systems that need to run for a long time, stay stable, scale well, and handle sustained traffic.</p><p>So the JVM accepts a higher startup cost in exchange for stronger optimization later. Once the system warms up, the JVM can become very fast, especially for workloads that run for a long time and have repetitive patterns.</p><h3><strong>Memory management</strong></h3><p>Another reason Java often feels heavier at startup is memory management. The JVM typically initializes memory areas such as:</p><ul><li><p>Heap</p></li><li><p>Stack</p></li><li><p>Metaspace</p></li><li><p>Garbage Collection structures</p></li></ul><p>In many production systems, Java is also configured with parameters such as <code>-Xms</code> and <code>-Xmx</code> to define the initial and maximum memory size. This helps the system remain stable during long-running execution, but it also increases startup time and initial resource usage.</p><p>Node.js usually has a lighter startup footprint, especially for small or medium applications. However, actual memory usage still depends on code, dependencies, caching, and workload. Saying &#8220;Node.js is always light&#8221; is too absolute, but saying &#8220;<strong>Node.js is usually lighter at startup</strong>&#8221; is reasonable.</p><h3><strong>Meaning for serverless</strong></h3><p>This is where Node.js often has a very clear advantage.</p><p>In serverless environments such as AWS Lambda, startup latency &#8212; or <strong>cold start</strong> &#8212; directly affects user experience. Because Node.js typically starts faster, it is often chosen for:</p><ul><li><p>Short APIs</p></li><li><p>Webhooks</p></li><li><p>Small jobs</p></li><li><p>Simple logic that needs to respond quickly</p></li></ul><p>That said, it is also not correct to say Node.js is &#8220;the winner in all cases&#8221; or &#8220;always the best.&#8221; Java can absolutely be used in serverless, especially when optimized correctly. There are also now techniques that significantly reduce Java cold starts, so Java is no longer the &#8220;too slow&#8221; option it used to be.</p><div><hr></div><h2><strong>4. What improvements has Java made to compete with Node.js for I/O-bound workloads?</strong></h2><p>If Node.js was once considered the strongest choice for I/O-bound workloads, Java has now made major improvements that close the gap &#8212; and in some cases even outperform it. The two most important directions are <strong><a href="https://quangchientran.substack.com/i/195542055/reactive-a-solution-that-is-not-easy-to-swallow">Reactive Programming</a> with WebFlux</strong> and <strong><a href="https://open.substack.com/pub/quangchientran/p/java-virtual-threads-the-end-of-the?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Virtual Threads</a></strong> in Java 21.</p><h3><strong>Reactive Programming</strong></h3><p>WebFlux is a <strong>non-blocking reactive</strong> model in the Spring ecosystem. Its goal is to handle many concurrent requests without keeping one blocking thread per request, as in the traditional model.</p><p>The strengths of WebFlux include:</p><ul><li><p>It fits systems with a lot of I/O.</p></li><li><p>It uses system resources very efficiently.</p></li><li><p>It increases throughput when there are many concurrent connections.</p></li><li><p>It is a great fit for streaming workflows or services that call each other frequently.</p></li></ul><p>The way WebFlux works can remind us of Node.js because both follow an event-driven, non-blocking style. However, WebFlux is not &#8220;<s>Node.js in Java</s>.&#8221; It is a reactive approach built on the Spring ecosystem, often running on a non-blocking runtime such as Netty.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NKN4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NKN4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NKN4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/525d7191-387b-40df-927c-f80f31334c9f_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NKN4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>The important thing is that <strong>WebFlux does not automatically solve CPU-bound problems</strong>. If you put heavy computation on the same event loop or structure your reactive pipeline poorly, you can still block the system. So WebFlux is strong for I/O-bound workloads, but it is not the answer for every kind of workload.</p><p>In short, WebFlux is a good fit when you need:</p><ul><li><p>A high number of concurrent requests.</p></li><li><p>More I/O than computation.</p></li><li><p>End-to-end non-blocking behavior.</p></li><li><p>Efficient thread usage.</p></li></ul><p>The trade-off is that it is more complex. Reactive code is often harder to read, harder to debug, and requires the team to be comfortable with data flow, backpressure, and asynchronous thinking.</p><h3><strong>Virtual Threads</strong></h3><p>Starting with Java 21, Java introduced <strong><a href="https://quangchientran.substack.com/i/195542055/virtual-threads">Virtual Threads</a></strong>, one of the most important features since Java 8, especially with its deep integration into Spring Boot 3.2.</p><p>Virtual Threads let you create <strong>a huge number of lightweight threads, but these threads do not map 1:1 to OS threads</strong>. Instead, they are managed by the JVM scheduler and shared across a smaller number of underlying OS threads.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YNbR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YNbR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YNbR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YNbR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>This brings a major benefit:</p><ul><li><p>You can still write code in the familiar blocking style.</p></li><li><p>But the resource cost is much lower than traditional threads.</p></li><li><p>When I/O happens, a virtual thread can be paused so the carrier thread can be used by another task.</p></li></ul><p>The best thing about Virtual Threads is that they allow Java to <strong>keep code simple while still scaling well for I/O-heavy workloads</strong>. That is why many developers &#8212; myself included &#8212; consider it one of the most important Java improvements in years.</p><p>Compared with reactive programming, Virtual Threads are usually easier for most developers to adopt because:</p><ul><li><p>You do not need to fully switch to reactive thinking.</p></li><li><p>You do not need to chain callbacks or pipelines everywhere.</p></li><li><p>The code looks close to traditional synchronous style.</p></li></ul><p>That said, Virtual Threads are still not a silver bullet for everything. They help a lot with I/O-bound workloads, but for heavy CPU-bound tasks, you still need proper architecture, task splitting, or parallel execution where appropriate.</p><h3><strong>Compared with Node.js</strong></h3><p>If we compare modern Java with Node.js, the discussion is no longer as simple as &#8220;<s>Node.js is faster than Java for I/O-bound workloads</s>.&#8221;</p><p>Node.js is still very strong in:</p><ul><li><p>Fast startup</p></li><li><p>A simple event loop model</p></li><li><p>A unified JavaScript ecosystem from backend to frontend</p></li><li><p>Small, lightweight services that need fast responses</p></li></ul><p>Meanwhile, Java now offers:</p><ul><li><p>WebFlux for reactive, non-blocking programming</p></li><li><p>Virtual Threads for simple code that still scales well</p></li><li><p>A highly optimized JVM for long-running systems</p></li></ul><p>So Java has become a very competitive option for I/O-bound workloads, especially when the team wants readable code, maintainability, and the strength of the Spring ecosystem.</p><div><hr></div><h2><strong>5. Why is JavaScript different on the frontend and backend?</strong></h2><p>I&#8217;m not the only one who has asked this question. It is a very natural one, and it reveals a common misunderstanding: many people assume that if it is all JavaScript, it should behave the same everywhere. In reality, JavaScript itself is not the whole story. The <strong>runtime</strong> and the <strong>execution environment</strong> matter just as much.</p><p>JavaScript on the frontend and backend uses the same language, but they run in two different worlds:</p><ul><li><p>The frontend runs in the <strong>browser runtime</strong>.</p></li><li><p>The backend runs in the <strong>Node.js runtime</strong>.</p></li></ul><p>That difference is what gives them very different capabilities and limitations.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xanj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xanj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!Xanj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!Xanj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!Xanj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xanj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Minimal cover showing frontend and backend JavaScript worlds&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Minimal cover showing frontend and backend JavaScript worlds" title="Minimal cover showing frontend and backend JavaScript worlds" srcset="https://substackcdn.com/image/fetch/$s_!Xanj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!Xanj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!Xanj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!Xanj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35e0f557-8826-4828-9cd5-54648123fcfa_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3><strong>Frontend and backend use different runtimes</strong></h3><p>When you write frontend code with React, Vue, Angular, or plain JavaScript, your code runs in the browser. The browser provides many APIs for user interaction and UI work, such as:</p><ul><li><p><code>alert()</code></p></li><li><p><code>window</code></p></li><li><p><code>document</code></p></li><li><p>DOM manipulation</p></li><li><p>Mouse, keyboard, and scroll events</p></li></ul><p>That is why <code>alert()</code> works in the frontend. It is part of the <strong>browser API</strong>, not part of JavaScript itself.</p><p>By contrast, when JavaScript runs in Node.js, it does not have objects like <code>window</code> or <code>document</code>. Node.js is designed for server environments, so it provides different APIs, such as:</p><ul><li><p>Working with the filesystem</p></li><li><p>Handling network operations</p></li><li><p>Creating servers</p></li><li><p>Reading environment variables</p></li><li><p>Accessing process information</p></li><li><p>Using other server-side libraries</p></li></ul><p>That is why you cannot call <code>alert()</code> in a Node.js backend. That API simply does not exist in that runtime.</p><h3><strong>Node.js is not &#8220;just JavaScript&#8221;</strong></h3><p>One easy misconception is to think that Node.js is simply &#8220;<strong>JavaScript running somewhere else</strong>.&#8221; In reality, Node.js is a <strong>runtime environment</strong> for JavaScript.</p><p>Besides the JavaScript engine itself, Node.js also comes with:</p><ul><li><p>A <strong>runtime</strong> for executing code</p></li><li><p>A <strong>standard library</strong> for system-level tasks</p></li><li><p><code>npm</code>, the most common package manager that comes with the ecosystem</p></li></ul><p>Because of that, backend JavaScript can do things the browser cannot, such as reading files, creating a TCP server, or connecting to a database.</p><h3><strong>Why can&#8217;t the browser connect directly to a database?</strong></h3><p>Frontend applications cannot &#8212; and should not &#8212; connect directly to a production database for several reasons.</p><p>The first reason is <strong>security</strong>. If the browser could connect directly to the database, you would have to expose database credentials on the client side. That would be extremely dangerous, because users could inspect, modify, or abuse those credentials.</p><p>The second reason is <strong>system architecture</strong>. In modern web applications, the frontend and the database should not talk directly. Instead, the flow usually looks like this:</p><blockquote><p><strong>Frontend &#8594; Backend API &#8594; Database &#8594; Backend API &#8594; Frontend</strong></p></blockquote><p>This approach helps:</p><ul><li><p>Protect sensitive credentials.</p></li><li><p>Control access permissions.</p></li><li><p>Handle validation.</p></li><li><p>Improve logging and auditing.</p></li><li><p>Keep client and server responsibilities separate.</p></li></ul><p>So the most accurate thing to remember is this:</p><blockquote><p><strong>It is not JavaScript itself that decides what you can do &#8212; it is the runtime and the execution environment.</strong></p></blockquote><div><hr></div><h2><strong>Conclusion</strong></h2><p>Looking back at these five questions, the most important thing is not deciding whether Node.js or Java is &#8220;better.&#8221; <strong>The real point is that each platform is strong in a different kind of workload</strong>. Node.js shines with the event loop, non-blocking I/O, fast startup, and a unified frontend-backend JavaScript ecosystem. Java, on the other hand, has come a long way with WebFlux and Virtual Threads, making it much more competitive for I/O-bound workloads than it used to be.</p><p>If you understand the real nature of the runtime, event loop, cluster, virtual threads, and the limitations of the browser compared with Node.js, it becomes much easier to choose the right technology for each situation. And instead of asking who is &#8220;better,&#8221; the more valuable question is: <strong>for this workload, this team, and this set of requirements, which choice is the least risky and the easiest to operate?</strong></p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Under the Hood: A Deep Dive into Processes, Threads, and CPU Architecture]]></title><description><![CDATA[A Comprehensive Guide to Processes, Threads, Multitasking, and CPU Cache Architecture.]]></description><link>https://quangchientran.substack.com/p/under-the-hood-a-deep-dive-into-processes</link><guid isPermaLink="false">https://quangchientran.substack.com/p/under-the-hood-a-deep-dive-into-processes</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Tue, 28 Apr 2026 05:16:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!UtWo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Foundational knowledge</strong> has always been an <strong>important</strong> part of the programming world, and even now, <strong>AI</strong> has crept into almost every job of a programmer. By understanding the <strong>fundamentals</strong>, you will find it easier to develop, debug, and also use AI more effectively.</p><p>Today, I will have a <strong>deep dive</strong> into <strong>Process</strong> and <strong>Thread</strong>, things that lie under our operating system layer, one of the foundational pieces of knowledge you must master when working with computers, helping you understand what really happens underneath when an application runs. Let&#8217;s get started.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UtWo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UtWo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 424w, https://substackcdn.com/image/fetch/$s_!UtWo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 848w, https://substackcdn.com/image/fetch/$s_!UtWo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 1272w, https://substackcdn.com/image/fetch/$s_!UtWo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UtWo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png" width="1024" height="572" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:572,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:708919,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.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_!UtWo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 424w, https://substackcdn.com/image/fetch/$s_!UtWo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 848w, https://substackcdn.com/image/fetch/$s_!UtWo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 1272w, https://substackcdn.com/image/fetch/$s_!UtWo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F653dee67-898e-4f75-9d62-fe8f0abda64f_1024x572.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"></div></div></a></figure></div><h2><strong>What is a Process ?</strong></h2><p>Simply put, a <strong>process</strong> is a software program that is being executed on a computer. One software can create many different <strong>processes</strong>. On a computer, there can be many processes from many different software programs coexisting at the same time.</p><p>To make it easier to understand, with Windows, when you run a web browser like Chrome, it will create many different <strong>processes</strong> for each tab, extension, and system component, or when you launch a game, it will create a separate process.</p><p>Each <strong>process</strong> has its own <strong>process ID</strong>, its own data and state. Each process works in its own <strong>memory space</strong>, and cannot directly access the data of another process unless there is a sharing mechanism allowed by the operating system (<strong>IPC - Inter-Process Communication</strong>).</p><p>To store the data of a process, the OS will use a data structure called <strong>Process Control Block (PCB)</strong>. Each PCB is associated with a separate <strong>PID</strong>. The PCB includes the following information:</p><ul><li><p><strong>Process ID (PID)</strong>: An integer that identifies the process.</p></li><li><p><strong>State</strong>: The current state of the process. A process isn't always "Running." It might be <strong>Ready</strong> (waiting for its turn), <strong>Waiting</strong> (waiting for you to click something or a file to load), or <strong>Terminated</strong>.</p></li><li><p><strong>Pointer</strong>: Information linking to related processes.</p></li><li><p><strong>Priority</strong>: The priority of the process, helping the processor determine the execution order.</p></li><li><p><strong>Program Counter</strong>: A pointer storing the address of the next instruction to be executed by the process. This is vital for <strong>Context Switching</strong>. Since the CPU jumps between processes thousands of times per second, the PC acts like a "bookmark" so the CPU knows exactly where it left off when it returns to that process.</p></li><li><p><strong>CPU Registers</strong>: The registers the process needs to use for execution. These store the temporary data (the "math" being done at that exact microsecond).</p></li><li><p><strong>I/O Information</strong>: Information about the read/write devices the process needs to use.</p></li><li><p><strong>Accounting Information</strong>: Contains information about CPU usage such as time used, identification.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0NeI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0NeI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!0NeI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!0NeI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!0NeI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0NeI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:645687,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!0NeI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!0NeI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!0NeI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!0NeI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94bc2df9-b22d-4c6e-b967-31bf4fe4d50b_1024x559.png 1456w" sizes="100vw"></picture><div class="image-link-expand"></div></div></a></figure></div></li></ul><h2><strong>What is a Thread ?</strong></h2><p>A thread is a lightweight unit of execution within a process. If a process is a house, threads are the people living inside&#8212;they share common spaces like the <strong>heap</strong> and the process <strong>address space</strong>, but each thread has its own private <strong>stack</strong> and <strong>register state</strong>.</p><ul><li><p><strong>Smallest unit</strong>: It is the smallest sequence of programmed instructions that a scheduler can schedule independently.</p></li><li><p><strong>Shared resources</strong>: Threads share code, data, and OS resources such as open files. This makes communication fast but also creates <strong>race conditions</strong> when two threads access and modify the same data without proper synchronization.</p></li><li><p><strong>Efficiency</strong>: Context switching between threads is usually cheaper than switching between processes because the address space often stays the same.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lIQF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lIQF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!lIQF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!lIQF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!lIQF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lIQF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:711295,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.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_!lIQF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!lIQF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!lIQF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!lIQF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12e3cd7c-3f23-4263-9d03-085f7a9af18a_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>Hardware vs. Software Threads</h4><ul><li><p><strong>Hardware threads</strong>: These are execution contexts exposed by the CPU. In your example, an Apple M4 with 10 cores can provide 10 hardware threads. This affects how many threads can run in true parallelism. A &#8220;Hardware Thread&#8221; is essentially a set of registers on the CPU core that allows it to hold the state of a software thread.</p></li><li><p><strong>OS/software threads</strong>: These are managed by the kernel. You can create many software threads, and the OS time-slices them across the available hardware threads.</p></li></ul><p><strong>The Java Example</strong></p><p>Historically, one Java thread usually mapped to one OS thread (platform threads). Modern <strong>virtual threads</strong> (Project Loom) let many Java threads run on a smaller number of OS threads, which helps high-scale applications become more efficient.</p><h4>How a Thread is Created ?</h4><p>When each thread is created, it has its own execution state, including an <strong>Instruction Pointer (IP)</strong>, which determines the location of the next instruction the thread will execute.</p><p>Thanks to this separate execution state, when the CPU performs a <strong>Context Switch</strong> between threads, each thread can continue right from where it left off instead of starting over from the beginning.</p><h4>The Thread Control Block (TCB)</h4><p>Just as a process has a PCB, a thread typically has a <strong>TCB</strong> or an equivalent thread-specific structure. It is usually smaller and lighter than a PCB.</p><p>A TCB may contain:</p><ul><li><p>Thread ID.</p></li><li><p>Stack Pointer.</p></li><li><p>Instruction Pointer.</p></li><li><p>State.</p></li><li><p>Register values.</p></li></ul><p><strong>Why it is &#8220;Cheaper&#8221;</strong></p><p>Switching between threads in the same process is usually faster than switching between processes because the OS does not need to switch to a different address space. Threads in the same process share the same memory map, so the overhead is lower.</p><h2><strong>Multi-thread</strong></h2><p><strong>Multi-threading</strong> is the ability of a CPU or a single process to provide multiple threads of execution concurrently. Instead of following just one line of instructions, the process can split into multiple execution paths.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!297p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!297p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!297p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!297p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!297p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!297p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:783921,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.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_!297p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!297p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!297p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!297p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889812cf-1885-482a-9fff-600fb9447e5d_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>Advantages</h4><ul><li><p><strong>Non-blocking UI</strong>: Essential for modern applications. The main thread handles user input such as clicks and scrolling, while worker threads handle heavy tasks such as API calls or database queries.</p></li><li><p><strong>Better resource utilization</strong>: On a multi-core processor, multi-threading allows the OS to run multiple tasks in parallel when possible.</p></li><li><p><strong>Economy</strong>: Threads are cheaper to create than processes because they do not require a completely new memory space. They share the existing heap within the same process.</p></li></ul><h4>Disadvantages</h4><ul><li><p><strong>Race conditions</strong>: These happen when two threads read or write the same shared variable at the same time.</p><ul><li><p>Example: both threads see a balance of $100, both add $10, and instead of getting $120, the final result might be $110 because one update overwrites the other.</p></li></ul></li><li><p><strong>Deadlock</strong>: Thread A holds Resource 1 and waits for Resource 2, while Thread B holds Resource 2 and waits for Resource 1. Both wait forever.</p></li><li><p><strong>Starvation</strong>: Low-priority threads may never get CPU time if higher-priority threads keep taking the processor.</p></li><li><p><strong>Testing complexity</strong>: Multi-threaded bugs are often <strong>Heisenbugs</strong> &#8212; they disappear when you try to observe them because timing changes during debugging.</p></li><li><p><strong>The invisible cost</strong>: Context switching and memory synchronization, such as <code>volatile</code> or <code>synchronized</code> in Java, add overhead that can make a simple program slower than a single-threaded one.</p></li></ul><h4>Why It Does Not Scale Forever</h4><p>Multi-threading does not always make programs faster. This is formally described by <strong>Amdahl&#8217;s Law</strong>, which says that the speedup of a program is limited by its serial part &#8212; the part that cannot be parallelized. If 20% of your code must still run sequentially, then your program can never be more than 5x faster, no matter how many threads you add. You can find more information about this law online or ask AI, it&#8217;s quite easy to understand.</p><p>Another hidden cost is <strong>context switching overhead</strong>. When the CPU moves from Thread A to Thread B, it must save the current state, such as registers and the stack pointer, and then load the new one. If you have too many threads, the CPU may spend more time switching than doing useful work.</p><h2><strong>Multi-process Model</strong></h2><p>Multi-process is a model in which a program or system uses multiple independent processes to handle work. Each process lives in its own isolated virtual address space, has its own state, and does not directly share data with other processes like threads do. Because of this isolation, when one process crashes (for example with a segmentation fault), the other processes can often continue running normally.</p><p>Simply put, multi-process is like splitting a large application into multiple separate &#8220;work rooms&#8221;. Each room has its own task, its own people, its own documents, and is less dependent on the others. This makes the system safer and easier to isolate.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5Voh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5Voh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!5Voh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!5Voh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!5Voh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5Voh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/92647f9b-cb50-45b5-a987-753426483da2_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:783713,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.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_!5Voh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!5Voh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!5Voh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!5Voh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92647f9b-cb50-45b5-a987-753426483da2_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>Examples:</p><ul><li><p>A browser can use multiple processes for tabs, extensions, and network.</p></li><li><p>A server like Nginx or PostgreSQL (<a href="https://open.substack.com/pub/quangchientran/p/deep-inside-postgres-processes-forking?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Process by Connection mechanism</a>) can also use multiple processes to handle different tasks.</p></li><li><p>A Python program can create multiple processes to handle heavy tasks on multiple CPU cores.</p></li></ul><h4>Why use multi-process?</h4><p>Use multi-process when:</p><ul><li><p>The work can be broken down into many independent parts.</p></li><li><p>You want to take advantage of multiple CPU cores to improve performance.</p></li><li><p>You want fault isolation, to avoid one part of a failure affecting the entire application.</p></li><li><p>You want to avoid the limitations of threads in some runtimes or languages.</p></li></ul><h4>The GIL (Global Interpreter Lock):</h4><p>In languages like Python or Ruby, a global interpreter lock (GIL/GVL) prevents multiple threads from executing bytecode at the same time. To get true parallelism on a multi-core CPU, developers often use multi-processing instead of multi-threading.</p><h4>Advantages:</h4><ul><li><p>Better resource isolation than multi-thread.</p></li><li><p>One process failure does not bring down the entire system.</p></li><li><p>Can truly utilize multiple CPU cores.</p></li><li><p>Good fit for CPU-bound tasks.</p></li></ul><h4>Copy-on-Write (CoW):</h4><p>Although each process has its own memory, modern operating systems use a trick called Copy-on-Write. When a process forks, the OS does not immediately copy all of its memory. Both processes share the same physical memory pages until one of them writes to a page. Only then is that page actually copied. This makes multi-processing more efficient than it might sound at first.</p><h4>Disadvantages:</h4><ul><li><p>Creating and managing processes is more expensive than threads, because spawning a process requires heavier system calls to the kernel.</p></li><li><p>Communication between processes is more complex, because their memory spaces are separate.</p></li><li><p>It consumes more memory because each process still has its own stacks, heaps, and libraries.</p></li></ul><h4>IPC complexity:</h4><p>Because processes cannot directly see each other&#8217;s memory, they must use Inter-Process Communication (IPC) mechanisms:</p><ul><li><p>Pipes &amp; sockets: sending data like a stream or a phone call.</p></li><li><p>Shared memory: setting up a common memory region that both processes can access.</p></li><li><p>Message queues: leaving messages in a mailbox for other processes to read later.</p></li></ul><h2><strong>Multitasking</strong></h2><p>Multitasking is the ability of an operating system to manage multiple tasks, such as processes or threads, concurrently. Even on a machine with multiple cores, the operating system still relies on multitasking to make many programs appear to run at the same time. In practice, the OS divides CPU time into small slices and alternates between tasks so quickly that it creates the illusion of full simultaneity.</p><p>At the center of multitasking is <strong>Context Switching</strong>. A context switch happens when the CPU stops running one process or thread and switches to another. Before switching, the operating system must save the current execution state, and when the task runs again later, it restores that state so execution can continue from exactly where it left off.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QJk-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QJk-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!QJk-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!QJk-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!QJk-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QJk-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:807589,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.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_!QJk-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!QJk-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!QJk-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!QJk-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4de1779-0c62-407d-a895-b268e7c81b43_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>You can think of the CPU like a chef cooking many meals at once. Before moving away from one dish, the chef remembers the temperature, timer, and cooking status. When returning later, the chef checks those notes and continues from the right point instead of starting over.</p><p>What gets saved during a context switch usually includes:</p><ul><li><p><strong>Program Counter</strong>: the next instruction to execute.</p></li><li><p><strong>CPU Registers</strong>: temporary values currently being used by the CPU.</p></li><li><p><strong>State information</strong>: whether the task is Ready, Running, Waiting, or another state.</p></li><li><p><strong>I/O information</strong>: any relevant input/output details.</p></li><li><p><strong>Accounting information</strong>: usage statistics such as CPU time.</p></li></ul><h4>The Cost of Switching</h4><p>Context switching is essential, but it is not free. It adds overhead, which means the CPU spends time on housekeeping instead of useful work. Saving and restoring state takes time, and frequent switches can reduce overall performance.</p><p>There is also a cache effect. When the CPU switches from one task to another, the cache may still contain data from the previous task. The new task may suffer cache misses and need to fetch data from the slower main memory, which adds more delay. This is one reason why too many context switches can hurt performance.</p><h4>Preemptive vs. Cooperative</h4><p>Modern operating systems usually use <strong>preemptive multitasking</strong>. In this model, the OS is in control and can forcibly stop a task when its time slice expires. This helps keep the system responsive and prevents one task from monopolizing the CPU.</p><p>Older systems sometimes used <strong>cooperative multitasking</strong>. In that model, tasks had to voluntarily give up control. If one task froze or misbehaved, the whole system could become unresponsive. That is why preemptive multitasking became the standard in modern operating systems.</p><h4>Hardware Support</h4><p>Modern CPUs also provide hardware features that help context switching happen more efficiently. The CPU can save and restore register state quickly, which reduces some of the cost of switching. Even so, context switching still has a real performance price, especially when it happens too often.</p><p>In short, multitasking is the big picture, while context switching is the mechanism that makes it work. Multitasking allows many tasks to share one CPU over time, while context switching is the actual process of moving from one task to another and back again.</p><h2><strong>Scheduler</strong></h2><p>The <strong>scheduler</strong> is a core component of the operating system kernel that decides which process or thread gets to run on the CPU at any given moment. In simple terms, it is like the director of a stage performance: it decides who gets on stage first, who has to wait, and how long each actor gets to perform.</p><p>The scheduler does not just choose <em>what</em> runs, but also <em>for how long</em>. When a task&#8217;s time slice ends, or when it needs to wait for I/O, the scheduler moves the CPU to another task so the system stays responsive and does not waste CPU time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oeMX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oeMX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!oeMX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!oeMX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!oeMX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oeMX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:823975,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.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_!oeMX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!oeMX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!oeMX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!oeMX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfdd86ba-4f18-4b6c-9c28-14fe9abbd22d_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>The Three Levels of Scheduling</h4><p>In modern operating systems, scheduling is usually split into three levels:</p><ul><li><p><strong>Long-term scheduler</strong>: decides which jobs are admitted into the system from disk into memory. It controls the degree of multiprogramming.</p></li><li><p><strong>Short-term scheduler</strong>: also called the <strong>CPU scheduler</strong>, this is the one that picks a task from the Ready Queue and gives it CPU time. It runs very frequently and must be extremely fast.</p></li><li><p><strong>Medium-term scheduler</strong>: handles <strong>swapping</strong>. When memory is under pressure, it can temporarily move a process out of RAM and bring it back later.</p></li></ul><p>These three roles work together to balance performance, memory usage, and responsiveness.</p><h4>Process States and Queues</h4><p>A process usually moves through a small number of states:</p><ul><li><p><strong>New</strong>: the process is being created.</p></li><li><p><strong>Ready</strong>: the process is ready to run and is waiting in the Ready Queue.</p></li><li><p><strong>Running</strong>: the process is currently using a CPU core.</p></li><li><p><strong>Waiting</strong> or <strong>Blocked</strong>: the process cannot run because it is waiting for a slow event, such as disk I/O or user input.</p></li><li><p><strong>Terminated</strong>: the process has finished and is being cleaned up.</p></li></ul><p>A common point of confusion is that a process usually does <strong>not</strong> go directly from Waiting to Running. It must first return to the Ready Queue and wait for the short-term scheduler to pick it again.</p><h4>Scheduling Queues</h4><p>The operating system uses different queues to organize processes and threads:</p><ul><li><p><strong>Job Queue</strong>: contains jobs that have not yet been admitted into memory.</p></li><li><p><strong>Ready Queue</strong>: contains processes or threads that are ready to run on the CPU.</p></li><li><p><strong>Device Queue</strong>: contains processes or threads waiting for I/O devices such as disk, network, or other peripherals.</p></li></ul><p>When a process changes state, it moves to the appropriate queue. For example, if a running process needs to read from disk, it leaves the CPU and moves to the Device Queue. When the I/O completes, it returns to the Ready Queue and waits for CPU time again.</p><h4>Scheduling Criteria</h4><p>When choosing a scheduling algorithm, the operating system usually balances several goals:</p><ul><li><p><strong>CPU utilization</strong>: keep the CPU busy as much as possible.</p></li><li><p><strong>Throughput</strong>: finish as many jobs as possible in a given time.</p></li><li><p><strong>Turnaround time</strong>: reduce the time from New to Terminated.</p></li><li><p><strong>Response time</strong>: reduce the delay between a user action and the first visible reaction.</p></li><li><p><strong>Fairness</strong>: ensure that no task is starved of CPU for too long.</p></li><li><p><strong>Waiting time</strong>: reduce the time a task spends waiting before it gets CPU time.</p></li></ul><p>The best choice depends on the system&#8217;s goal. A server may care more about throughput and CPU utilization, while a desktop OS cares more about response time and fairness.</p><h4>Scheduling Algorithms</h4><p>Several scheduling algorithms are commonly used:</p><ul><li><p><strong>First Come, First Serve (FCFS)</strong>: the first task to arrive is the first task to run.</p></li><li><p><strong>Round Robin (RR)</strong>: each task gets a fixed time slice, then the CPU moves to the next task.</p></li><li><p><strong>Priority Scheduling</strong>: higher-priority tasks run first.</p></li><li><p><strong>Shortest Job First (SJF)</strong>: tasks with less work are prioritized.</p></li><li><p><strong>Shortest Remaining Time</strong>: the task with the least remaining processing time is selected.</p></li><li><p><strong>Multi-level Queue</strong>: the system is divided into multiple queues, each with its own scheduling policy.</p></li></ul><p>Each algorithm has trade-offs between responsiveness, fairness, and efficiency. Round Robin is simple and fair, but may create more context switching. SJF can reduce average waiting time, but it is harder to use in practice because the OS must estimate job length.</p><h4>Multi-level Feedback Queue</h4><p>Most modern operating systems do not rely on a simple scheduling model alone. A common real-world approach is the <strong>Multi-level Feedback Queue (MLFQ)</strong>.</p><p>In MLFQ, a new task typically starts in a high-priority queue with a short time slice. If it behaves like a quick interactive task, it stays near the top. If it keeps using too much CPU time, the OS gradually moves it to lower-priority queues with longer time slices. This keeps the user interface responsive while still allowing large jobs to complete.</p><h4>CPU-bound and I/O-bound Workloads</h4><p>Different workloads need different scheduling behavior:</p><ul><li><p><strong>CPU-bound tasks</strong> spend most of their time doing computation.</p></li><li><p><strong>I/O-bound tasks</strong> spend most of their time waiting for disk, network, or other devices.</p></li></ul><p>Schedulers often try to favor short interactive or I/O-bound tasks so the system feels responsive, while still making sure CPU-bound tasks eventually get enough processing time.</p><h2><strong>Shared Memory</strong></h2><p>Shared Memory is one of the fastest methods for <strong>Inter-Process Communication (IPC)</strong>. Instead of sending data back and forth through the kernel, the operating system maps the same physical memory region into the virtual address spaces of multiple processes. That means the same data can be accessed directly by more than one process.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UwWv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UwWv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!UwWv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!UwWv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!UwWv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UwWv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:837886,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.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_!UwWv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!UwWv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!UwWv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!UwWv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cadb4bd-f9ee-4bac-96fe-e227693caccc_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>Shared Memory vs. Message Passing</h4><p>There are two main ways for processes to communicate:</p><ul><li><p><strong>Message passing</strong>: the OS acts like a mailman. Process A sends data to the kernel, and the kernel delivers it to Process B. This is safer, but it usually involves copying data more than once.</p></li><li><p><strong>Shared memory</strong>: the OS provides a shared region of memory that both processes can read and write directly. This avoids copying and is much faster, especially for large data.</p></li></ul><p>This is why shared memory is often the preferred choice when speed matters most.</p><h4>How It Works ?</h4><p>The operating system takes a physical block of RAM and maps it into the virtual address spaces of two or more processes.</p><p>For example:</p><ul><li><p>To Process A, the shared data might appear at address <code>0x1000</code>.</p></li><li><p>To Process B, the same physical memory might appear at address <code>0x5000</code>.</p></li></ul><p>Even though the virtual addresses are different, both processes are looking at the same physical memory underneath.</p><h4>Advantages</h4><ul><li><p><strong>Zero-copy communication</strong>: once the memory is mapped, data moves at memory speed instead of being copied through the kernel.</p></li><li><p><strong>Good for large data</strong>: shared memory is ideal for video frames, database buffers, and other large datasets.</p></li><li><p><strong>Low latency</strong>: it is often faster than pipes or sockets for frequent data exchange.</p></li></ul><h4>Disadvantages and Risks</h4><ul><li><p><strong>Synchronization burden</strong>: the OS does not manage access automatically, so developers must protect shared data using atomic operations, mutexes, semaphores, or locks.</p></li><li><p><strong>Complexity</strong>: if one process crashes while holding a lock, other processes may get stuck, or the shared data may become corrupted.</p></li><li><p><strong>Security</strong>: if permissions are not configured carefully, shared memory can become a risk for unauthorized data access.</p></li></ul><h4>Mutex vs. Semaphore</h4><p>It is helpful to distinguish between the common synchronization tools:</p><ul><li><p><strong>Mutex</strong>: like a key to a bathroom. Only one thread or process can hold it at a time.</p></li><li><p><strong>Semaphore</strong>: like a parking lot counter. It allows a fixed number of threads or processes to access a resource at the same time.</p></li></ul><p>Shared memory is powerful because it gives you speed, but it also gives you responsibility. The OS provides the shared space, but the application must make sure it is used safely and correctly.</p><h2><strong>CPU Caches</strong></h2><p>Modern CPUs use a <strong>cache hierarchy</strong> to bridge the huge speed gap between the CPU and main memory (DRAM). The closer a memory level is to the CPU core, the faster it is, but also the smaller it tends to be.</p><p>Typically, CPUs have three main cache levels:</p><ul><li><p><strong>L1</strong>: the smallest and fastest cache. It is usually split into:</p><ul><li><p><strong>L1i</strong> for instructions.</p></li><li><p><strong>L1d</strong> for data.</p></li></ul></li><li><p><strong>L2</strong>: larger than L1, but slightly slower. In many modern designs, it acts as a buffer for L1 and may be shared by a small cluster of cores.</p></li><li><p><strong>L3</strong>: also called the <strong>Last Level Cache (LLC)</strong>. It is much larger, usually measured in megabytes, and can be shared across multiple cores.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EfTq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EfTq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 424w, https://substackcdn.com/image/fetch/$s_!EfTq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 848w, https://substackcdn.com/image/fetch/$s_!EfTq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 1272w, https://substackcdn.com/image/fetch/$s_!EfTq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EfTq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png" width="675" height="256" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:256,&quot;width&quot;:675,&quot;resizeWidth&quot;:675,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!EfTq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 424w, https://substackcdn.com/image/fetch/$s_!EfTq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 848w, https://substackcdn.com/image/fetch/$s_!EfTq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 1272w, https://substackcdn.com/image/fetch/$s_!EfTq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ae29eb-313f-4196-a1a8-31edb484af1d_675x256.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>When the CPU needs data, it checks <strong>L1 first</strong>, then <strong>L2</strong>, then <strong>L3</strong>, and finally DRAM if the data is not found in cache. This layered design helps keep frequently used data close to the processor, which dramatically reduces average memory access time.</p><h4>Cache Lines: The Unit of Transfer</h4><p>Data is transferred between memory levels in fixed-size blocks called <strong>cache lines</strong>. On many modern CPUs, a cache line is typically <strong>64 bytes</strong>.</p><p>The reason is that <strong>when a CPU reads a variable from RAM, it doesn&#8217;t just fetch that single variable (byte by byte). Instead, it loads an entire 64-byte chunk surrounding that variable (this is the cache line) into the CPU cache (L1)</strong>. It does this because it predicts that, in most cases, nearby data will soon be used. The next time, those neighboring values are already in L1, so there&#8217;s no need to access RAM again.</p><p>This mechanism is based on the principle of <strong>spatial locality</strong>. If the CPU accesses a value in memory, it assumes that nearby values are likely to be accessed soon as well.</p><p>That&#8217;s why arrays are typically cache-friendly. Their elements are stored next to each other in memory, allowing the CPU to use cache lines efficiently. In contrast, linked lists often have nodes scattered throughout memory, making cache usage much less efficient.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F1iB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F1iB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!F1iB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!F1iB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!F1iB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F1iB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:964343,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/194205408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!F1iB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!F1iB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!F1iB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!F1iB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81285da8-f6fe-4194-bbba-97da8531c3f8_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>Cache Hits and Misses</h4><h4>When the CPU looks for data, one of two things happens:</h4><ul><li><p><strong>Cache hit</strong>: the data is found in cache, so execution continues quickly.</p></li><li><p><strong>Cache miss</strong>: the data is not found, so the CPU must fetch it from a lower level, usually a slower cache or DRAM.</p></li></ul><p>A cache miss is expensive. The CPU may have to wait dozens or even hundreds of cycles while the data is loaded. That is why good cache behavior can have a huge impact on performance.</p><h4>Set-Associativity and Tags</h4><p>Cache is not managed like a simple hash table. Instead, it uses a <strong>set-associative</strong> structure.</p><p>The CPU uses specific bits from the memory address to choose a cache <strong>set</strong>, then compares the <strong>tag</strong> to see whether the data is actually there. This design lets the hardware find data very quickly without scanning the entire cache.</p><p>Each cache line usually contains:</p><ul><li><p>The actual data.</p></li><li><p>A <strong>tag</strong> that identifies which memory block it belongs to.</p></li><li><p>Metadata such as validity and dirty state.</p></li></ul><h4>Write-Back and Dirty Cache Lines</h4><p>To stay fast, CPUs often use a <strong>write-back</strong> policy. When data is modified, the change is written to cache first, and the cache line is marked as <strong>dirty</strong>.</p><p>The main memory is not updated immediately. Instead, the data is written back later, usually when:</p><ul><li><p>the cache line is evicted, or</p></li><li><p>synchronization is forced by something like a memory barrier, <code>volatile</code>, or atomic operations.</p></li></ul><p>This approach reduces traffic to DRAM and improves performance, but it also means that cache and memory can temporarily hold different versions of the same data.</p><h4>Cache Coherency and False Sharing</h4><p>On multi-core CPUs, each core can have its own cache (L1, L2). This creates a data <strong>synchronization</strong> problem between CPU cores <strong>during computation</strong>: if Core 1 modifies data in its cache (within a cache line), how does Core 2 know that its copy is now outdated and needs updating?</p><p>When Core 1 modifies a variable <code>x</code>, it marks that cache line as &#8220;<strong>dirty</strong>&#8221; (modified). If Core 2 wants to modify another variable <code>y</code> that happens to reside on the <strong>same cache line</strong>, it can&#8217;t use its current copy anymore. Instead, it has to <strong>fetch the updated cache line again from L3 or RAM</strong>. This issue is commonly known as <strong>false sharing</strong>.</p><p>When two cores modify values on the same cache line, the cache line <strong>becomes inefficient because it has to be repeatedly reloaded</strong>, which is costly in terms of time. This constant back-and-forth movement is managed by a cache <strong>coherency protocol - MESI protocol</strong>. This protocol ensures that cores coordinate with each other so they don&#8217;t keep using stale data. In practice, it guarantees that all cores eventually see the most up-to-date version of shared data.</p><p><strong>False sharing</strong> is particularly dangerous because the code may look completely correct and free of obvious contention, yet still perform poorly due to how data is laid out in cache lines.</p><p><strong>How to solve it</strong></p><p>To solve false sharing, the most effective approach is to ensure that variables written independently by each core/thread do not end up on the same cache line.</p><p><strong>Padding:</strong> Separate data into its own cache line by inserting extra &#8220;<strong>junk</strong>&#8221; data, making sure important variables do not sit within the same 64 bytes (so they fall on different cache lines). In Java, you can use the <code>@Contended</code> annotation to apply this padding.</p><p><strong>Redesign data per thread:</strong> Give each thread/core its own local variable, do the computation independently, and then merge the results at the end instead of continuously writing to a shared memory region.</p><h4>Why Caches Matter</h4><p>Caches are one of the biggest reasons modern CPUs are fast. They reduce the average cost of memory access, help keep the CPU busy, and make repeated or nearby data access much cheaper.</p><p>But caches also introduce complexity. To write high-performance code, you need to think not only about algorithms, but also about memory access patterns, cache locality, cache coherency, and false sharing.</p><h2><strong>Conclusion</strong></h2><p>Once you understand <strong>process</strong>, <strong>thread</strong>, <strong>scheduler</strong>, and <strong>cache</strong>, you will see more clearly why some code runs fast, why some code runs slowly, why <strong>synchronization bugs</strong> happen, and why <strong>AI</strong> cannot completely replace <strong>system thinking</strong>. Foundational knowledge does not make you write code instead of AI, but it helps you know what to ask AI, and how to verify AI&#8217;s results.</p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Java Virtual Threads in Java 21: From Platform Threads to Scalable Concurrency]]></title><description><![CDATA[Discover how virtual threads in Java 21 work, why they scale better than platform threads, and when to use them instead of reactive code.]]></description><link>https://quangchientran.substack.com/p/java-virtual-threads-java-virtual-threads-explained</link><guid isPermaLink="false">https://quangchientran.substack.com/p/java-virtual-threads-java-virtual-threads-explained</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Sun, 26 Apr 2026 22:00:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!bj0V!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Concurrency</strong> in Java has long been a painful trade-off: either choose <strong>Platform Threads</strong>, which are easy to write but consume a lot of RAM and are hard to scale, or choose powerful <strong>Reactive</strong> code that makes your brain twist into knots.</p><p><strong>Virtual Threads</strong> were created to end that trade-off. They let you handle millions of requests with the simplest synchronous coding style. Let&#8217;s explore everything from the traditional thread model to the real power of virtual threads to see why this is such a turning point for Java. Let&#8217;s begin :) :)</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bj0V!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bj0V!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!bj0V!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!bj0V!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!bj0V!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bj0V!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2349564,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/195542055?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.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_!bj0V!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!bj0V!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!bj0V!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!bj0V!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F708e1167-3a79-4902-ab50-1839b4fb62fd_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"></div></div></a></figure></div><h2>Platform Thread</h2><h3>Mechanism</h3><p>Before talking about <strong>Virtual Threads</strong>, I need to understand how Java has handled threads up to now.</p><p>In Java, each thread you create (through <code>java.lang.Thread</code>) is almost mapped 1-to-1 with an operating system thread (OS thread). These threads are usually called <strong>platform threads</strong>. It sounds simple, but the cost is far from small.</p><h4>Why are platform threads called <strong>&#8220;</strong>heavyweight<strong>&#8221;</strong>?</h4><p>Each platform thread is not just a concept inside the JVM &#8212; it is tightly bound to operating system resources. When you create a thread, the operating system allocates a separate memory area for it called the <strong>thread stack</strong>. This is where the entire execution state of the thread is stored.</p><p>Inside this stack are <strong>stack frames</strong> &#8212; each frame corresponds to one function call. Each frame will contain:</p><ul><li><p>Return address</p></li><li><p>Local variables</p></li><li><p>Parameters</p></li><li><p>Intermediate data used for execution</p></li></ul><p>The stack works in the familiar <strong>LIFO (Last In, First Out)</strong> principle.</p><ul><li><p>Call a function &#8594; push a frame onto the stack</p></li><li><p>End the function &#8594; pop the frame off the stack</p></li></ul><p>The CPU uses a register called the <strong>stack pointer</strong> to track the &#8220;top&#8221; of the stack, and moves it up or down accordingly during push/pop operations.</p><h4>Where is the problem?</h4><p>The important point is: the stack size of each thread is decided as soon as the thread is created (for example through the JVM <code>-Xss</code> parameter). Even if some operating systems can allocate stack in a &#8220;use as you go&#8221; way (<strong>lazy allocation</strong>), each thread still has to reserve a maximum stack area in advance.</p><p>This leads to two major consequences:</p><ul><li><p>Each thread consumes a significant amount of memory</p></li><li><p>The thread is managed directly by the OS scheduler &#8594; <strong>high context-switching cost</strong></p></li></ul><p>When you scale to tens of thousands or hundreds of thousands of <strong>concurrent tasks</strong>, this model starts to struggle</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zZMV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zZMV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!zZMV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!zZMV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!zZMV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zZMV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1496417,&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://quangchientran.substack.com/i/195542055?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.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_!zZMV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!zZMV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!zZMV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!zZMV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ba1049-2625-48a7-a792-06b6c5af9232_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3>Problems with threads &#8212; when everything starts getting overloaded</h3><p>The thread stack mechanism sounds neat, but in reality it hides quite a few problems.</p><h4>StackOverflowError &#8211; a familiar error</h4><p>One direct consequence is: if a program uses recursion without a proper stopping point, or has too many nested function calls, the number of stack frames keeps increasing. Eventually, when the number of frames exceeds the thread stack limit, the JVM throws:</p><blockquote><p><strong>StackOverflowError</strong></p></blockquote><p>The important thing to understand is that this error happens only on a specific thread, not because of the total number of threads in the system. It simply means that thread has <strong>run out of room</strong> to store more function calls.</p><h4>OutOfMemoryError &#8211; when too many threads are created</h4><p>On the other hand, the problem is no longer inside one thread, but in the number of threads. Each thread needs its own stack area, which by default is often around a few hundred KB to 1 MB (depending on the JVM and OS). When you create thousands or tens of thousands of threads, the total memory usage rises very quickly.</p><p>At some point, the system can no longer allocate a new thread, and you will get the error:</p><blockquote><p><strong>OutOfMemoryError: unable to create new native thread</strong></p></blockquote><p>This reminds us of something very practical: threads are not <strong>free</strong>.</p><h4>Thread-per-request &#8211; a popular model with limits</h4><p>Because of how platform threads work, traditional servers often use this model: one request = one thread. This is very intuitive, easy to code, easy to debug, and works well at moderate scale (a few hundred to a few thousand concurrent requests).</p><p>But when you scale to hundreds of thousands or millions of simultaneous requests, the problems become obvious:</p><ul><li><p>Not enough memory to hold that many threads</p></li><li><p>Context switching between threads becomes extremely expensive</p></li></ul><p>At that point, the system slows down, then eventually gets overwhelmed.</p><h4>Reactive &#8211; a solution that is not easy to swallow</h4><p>To get past those limits, a common path is <strong>Reactive Programming</strong>. Instead of &#8220;holding&#8221; a thread for the whole lifetime of a request, the system will:</p><ul><li><p>Use <strong>non-blocking I/O</strong></p></li><li><p>Release the thread while waiting (for example: waiting for a database or API)</p></li><li><p>Continue processing when data becomes available (<strong>event-driven</strong>)</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NKN4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NKN4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NKN4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/525d7191-387b-40df-927c-f80f31334c9f_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:521574,&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://quangchientran.substack.com/i/195542055?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.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_!NKN4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!NKN4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F525d7191-387b-40df-927c-f80f31334c9f_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>Thanks to that, a small number of threads can handle a very large number of requests at the same time. It sounds great, but the price is complexity. You no longer write code in a sequential (<strong>synchronous</strong>) style &#8212; you must switch to asynchronous thinking:</p><ul><li><p>Code becomes harder to read (callback, chain, reactive stream&#8230;)</p></li><li><p>Debugging becomes harder (the flow is no longer linear)</p></li><li><p>Maintenance becomes difficult if the design is not tight</p></li></ul><p>And this is exactly the point where many teams struggle when applying reactive programming in large systems.</p><h2>Virtual threads</h2><p>After all the limitations of platform threads, <strong>Virtual Threads</strong> appear as a very different approach. At the API level, everything still looks familiar: you still work with <code>java.lang.Thread</code>. But underneath, the operating model has changed completely.</p><h3>No longer &#8220;one thread = one OS thread&#8221;</h3><p>The biggest difference is: a virtual thread is no longer permanently tied to a single OS thread throughout its lifetime. Instead, the JVM acts like a <strong>smart scheduler</strong>. When a virtual thread runs, it is temporarily assigned to an OS thread (often called a <strong>carrier thread</strong>).</p><p>But when the virtual thread encounters a <strong>blocking operation</strong> (for example: calling a database, reading a file, calling an API&#8230;), the JVM can:</p><ul><li><p>Pause that virtual thread</p></li><li><p>Release the OS thread</p></li><li><p>Use that OS thread to run another virtual thread</p></li></ul><p>When the data becomes available, the original virtual thread will be resumed on an OS thread (which may not be the same one as before)</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YNbR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YNbR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YNbR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1431455,&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://quangchientran.substack.com/i/195542055?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.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_!YNbR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!YNbR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03000895-f04a-4367-bd3b-63f579bb5fc6_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3>Fewer real threads, more work</h3><p>With this mechanism, you no longer need:</p><ul><li><p>10,000 OS threads to handle 10,000 requests</p></li><li><p>Just a small number of OS threads to handle a large number of virtual threads</p></li></ul><p>In other words, the JVM is doing <strong>multiplexing</strong>: many virtual threads &#8594; few OS threads</p><p><strong>Cheap enough to use freely</strong><br>Because it no longer needs a fixed native stack like a platform thread, a virtual thread has extremely low creation cost. You can create hundreds of thousands or even millions of virtual threads while still staying within acceptable resource limits &#8212; something almost impossible with platform threads. This is especially useful for systems with:</p><ul><li><p>Lots of I/O</p></li><li><p>Lots of waiting time (waiting time greater than CPU time)</p></li></ul><h3>Similar to virtual memory</h3><p>If this concept feels a bit &#8220;magic,&#8221; think of it like this: Virtual Threads are similar to how <strong>virtual memory</strong> works. Instead of forcing the program to deal directly with limited physical resources (RAM or OS threads), the JVM creates an abstraction layer:</p><ul><li><p>Hides the real limits</p></li><li><p>Distributes resources more flexibly</p></li><li><p>Makes the most of what is available</p></li></ul><p>The result is that you feel like <strong>resources are almost infinite</strong>, while underneath everything is still carefully optimized.</p><h3>How virtual threads work</h3><p>A virtual thread is not a standalone &#8220;real&#8221; thread, but is placed into an internal scheduling mechanism by the JVM.</p><p>You can picture it simply: the JVM keeps a queue of virtual threads ready to run. When an OS thread (<strong>carrier thread</strong>) becomes free, the JVM <strong>mounts</strong> a virtual thread onto it for execution.</p><h4>Mount / Unmount</h4><p>During execution, a virtual thread continuously goes through two states:</p><ul><li><p><strong>Mount</strong>: attached to a carrier thread to run</p></li><li><p><strong>Unmount</strong>: detached when it hits a point where it must wait (blocking I/O, sleep, &#8230;)</p></li></ul><p>The interesting part is: when a virtual thread is unmounted, the carrier thread is not kept waiting. Instead, that OS thread is immediately returned to the JVM to run another virtual thread. The original virtual thread simply waits until it is ready again, then gets mounted later.</p><h4>Using resources efficiently</h4><p>Thanks to this mechanism, a small number of OS threads can rotate to serve many virtual threads. Compared to the thread-per-request model:</p><ul><li><p>No thread is occupied doing nothing while waiting for I/O</p></li><li><p>CPU is used more continuously and efficiently</p></li><li><p>The number of required OS threads drops sharply, saving operating system resources</p></li></ul><p>In other words, the system no longer wastes resources just by waiting.</p><h4>Code stays sync, runtime is very async</h4><p>One extremely valuable point is: developers do not need to change how they write code. You can still write code in this style:</p><ul><li><p>Call functions sequentially</p></li><li><p>Use blocking I/O normally</p></li></ul><p>But underneath, the JVM is silently:</p><ul><li><p>Pausing the thread when needed</p></li><li><p>Switching to another task</p></li><li><p>Returning to the exact spot to continue</p></li></ul><p>That means: <strong>the experience feels synchronous, but the performance is close to async.</strong></p><p><strong>A common misunderstanding is that each virtual thread is assigned to the &#8220;least busy&#8221; OS thread</strong>. That is not actually how it works. The JVM does not fix this mapping. Instead, it continuously:</p><ul><li><p>Schedules</p></li><li><p>Pauses</p></li><li><p>Resumes virtual threads</p></li></ul><p>depending on each thread&#8217;s execution state. This flexibility is the key that helps virtual threads work well in systems with:</p><ul><li><p>Lots of I/O</p></li><li><p>Many concurrent requests</p></li><li><p>Waiting time taking up most of the workload</p></li></ul><h3>Virtual thread context switching is lighter</h3><p>When people hear &#8220;<strong>context switching</strong>&#8221;, they often think it simply means changing the currently running thread. But with OS threads, the story is much heavier.</p><h4>OS thread context switch &#8211; where is the cost?</h4><p>With traditional threads, every context switch requires the operating system to <strong>jump into kernel space</strong>. It is not just a matter of stopping thread A and running thread B &#8212; it also includes:</p><ul><li><p>Saving the entire CPU state of the current thread</p></li><li><p>Restoring the state of the next thread</p></li><li><p>Updating kernel management structures</p></li><li><p>Affecting CPU cache performance (the cache gets &#8220;cold&#8221;)</p></li></ul><p>If this happens repeatedly, the accumulated cost becomes very large and drags down system throughput.</p><h4>Virtual threads &#8211; moving the work into the JVM</h4><p>Virtual threads work differently. <strong>Instead of pushing the context-switching burden down into the kernel, the JVM handles most of this logic in user space</strong>. When a virtual thread hits blocking work (I/O, sleep,&#8230;), the JVM will:</p><ul><li><p>&#8220;<strong>Freeze</strong>&#8221; (<strong>park</strong>) the virtual thread</p></li><li><p>Store the necessary state in <strong>JVM memory</strong></p></li><li><p><strong>Unmount</strong> it from the carrier thread</p></li></ul><p>Most importantly: all of this happens without kernel intervention. The carrier thread is immediately <strong>reused to run another virtual thread</strong>, without going through a heavy OS-thread-style context switch.</p><h4>No need to carry the whole &#8220;machine&#8221; like an OS thread</h4><p>An OS thread always comes with a fixed native stack, and its state is always tied to the <strong>kernel</strong>. In contrast, a virtual thread does not need to keep a native stack for its entire lifetime and only stores what is necessary to resume execution.</p><p>Simply put:</p><ul><li><p>OS thread = carries the whole <strong>machine</strong></p></li><li><p>Virtual thread = carries only the <strong>enough-to-resume</strong> state</p></li></ul><p>Therefore, pausing and resuming a virtual thread is much lighter. One important thing to understand correctly: <strong>virtual threads do not eliminate context switching</strong>. They only:</p><ul><li><p>Reduce the number of times kernel-level switching is needed</p></li><li><p>Move most scheduling work into the JVM</p></li><li><p>Optimize for the &#8220;<em><strong>run &#8594; wait &#8594; run again</strong></em>&#8221; pattern</p></li></ul><p>In I/O-heavy systems, this is a huge difference. You can think of it like this:</p><ul><li><p>OS thread: every time you switch tasks, you have to &#8220;<strong>ask the operating system for permission</strong>,&#8221; go through all the procedures &#8594; expensive</p></li><li><p>Virtual thread: the JVM handles internal scheduling itself, <strong>using OS threads only as temporary workers &#8594; faster and more flexible</strong></p></li></ul><p>So virtual threads are not &#8220;more magical,&#8221; but simply avoid unnecessary expensive costs.</p><h3>Examples</h3><h4><strong>Platform Thread</strong></h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;16ed2fc4-ccdb-417c-b84f-dcea3b607a97&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class PlatformThreadExample {

    private final HttpClient client = HttpClient.newHttpClient();

    public String getCombinedData() throws Exception {
        String user = call("https://api.example.com/user/1");
        String orders = call("https://api.example.com/orders/1");
        return user + " | " + orders;
    }

    private String call(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();

        HttpResponse&lt;String&gt; response = client.send(request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }

    public static void main(String[] args) throws Exception {
        PlatformThreadExample app = new PlatformThreadExample();
        System.out.println(app.getCombinedData());
    }
}</code></pre></div><p>Here, <code>client.send(...)</code> is a <strong>blocking call</strong>. The running thread is held until the HTTP response returns, so if there are many concurrent requests, you will need many platform threads.</p><h4>Reactive programming</h4><p>The example below uses <strong>Spring WebFlux / Reactor.</strong> The goal is not to block a thread while waiting for I/O, but to chain processing steps using <code>Mono</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;8d518ce0-2bb4-44d9-9c72-6c431230244b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public class ReactiveExample {

    private final WebClient webClient = WebClient.create();

    public Mono&lt;String&gt; getCombinedData() {
        Mono&lt;String&gt; userMono = webClient.get()
                .uri("https://api.example.com/user/1")
                .retrieve()
                .bodyToMono(String.class);

        Mono&lt;String&gt; ordersMono = webClient.get()
                .uri("https://api.example.com/orders/1")
                .retrieve()
                .bodyToMono(String.class);

        return userMono.zipWith(ordersMono, (user, orders) -&gt; user + " | " + orders);
    }

    public static void main(String[] args) {
        ReactiveExample app = new ReactiveExample();

        app.getCombinedData()
                .subscribe(System.out::println);

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ignored) {
        }
    }
}</code></pre></div><p>Here, <code>Mono</code> does not represent a value that is immediately available, but a <strong>pipeline</strong> that will complete after. The code does not block a thread while waiting for the HTTP response, and the reactive framework will coordinate the continuation when data becomes available.</p><h4>Virtual Thread</h4><p>This example <strong>keeps the synchronous coding style</strong> like platform threads, but runs on virtual threads.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;92c5d1a8-942c-4b3b-b2fa-c4fb9ab98bb2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadExample {

    private final HttpClient client = HttpClient.newHttpClient();

    public String getCombinedData() throws Exception {
        String user = call("https://api.example.com/user/1");
        String orders = call("https://api.example.com/orders/1");
        return user + " | " + orders;
    }

    private String call(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();

        HttpResponse&lt;String&gt; response = client.send(request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }

    public static void main(String[] args) {
        VirtualThreadExample app = new VirtualThreadExample();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -&gt; {
                try {
                    System.out.println(app.getCombinedData());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}</code></pre></div><p>The important point is that the business logic is almost unchanged compared with platform threads, but instead of being &#8220;<strong>stuck</strong>&#8221; on an OS thread while waiting for I/O, the virtual thread can be temporarily parked by the JVM so the carrier thread can do other work.</p><h2>Notes and Best practices</h2><p>Even though virtual threads are very powerful, there are a few important points to understand correctly so you do not use them the wrong way.</p><h3>Still a Thread, but don&#8217;t treat it like the old kind</h3><p>A virtual thread is still a <code>Thread</code>, so you can use <code>start()</code>, <code>join()</code>, and so on as usual. However, that does not mean it behaves like a platform thread. Older APIs like <code>stop()</code> and <code>suspend()</code>, which have long been <strong>dangerous</strong> and <strong>deprecated</strong>, should be avoided even more with virtual threads.</p><h3>Concurrency problems do not disappear</h3><p>Virtual threads do <strong>not change the nature of concurrent programming</strong>. The usual problems still remain:</p><ul><li><p>Race condition</p></li><li><p>Deadlock</p></li><li><p>Visibility</p></li><li><p>Atomicity</p></li></ul><p>In other words: virtual threads help you run more work at once. But they do not make your code <strong>automatically correct</strong>. You still have to:</p><ul><li><p>Use locks when needed</p></li><li><p>Ensure safe publication</p></li><li><p>Design shared state carefully</p></li></ul><p><strong>The JVM will not &#8220;keep waiting&#8221; for virtual threads</strong><br>One thing that can be surprising: a virtual thread <strong>does not keep the JVM alive the way a non-daemon platform thread</strong> does. That means if the main thread ends, the JVM may shut down even while virtual threads are still running.</p><p>So for important tasks (writing data, processing transactions, sending events&#8230;), you need to:</p><ul><li><p>Join explicitly</p></li><li><p>Or manage lifecycle clearly (for example with executors, structured concurrency, &#8230;)</p></li></ul><p>Do not rely on threads the way you used to.</p><h3>Pinning &#8211; when a virtual thread gets &#8220;stuck&#8221; to an OS thread</h3><p>In some situations, a virtual thread cannot unmount from its carrier thread. When that happens, it is called <strong>pinned</strong>. Typical cases include:</p><ul><li><p>Running inside a <code>synchronized</code> block/method and then hitting blocking code</p></li><li><p>Calling a native method or foreign function</p></li></ul><p>When <strong>pinned</strong>, the <strong>carrier thread is kept occupied and cannot be returned to the JVM scheduler</strong>, which reduces the ability to scale significantly. Simply put: you end up close to the traditional thread model again, but without noticing it. For that reason, if your code uses a lot of <code>synchronized</code>, you should re-check:</p><blockquote><p><em>Do you really need the lock? Are you holding the lock while calling I/O?</em></p></blockquote><p>Some ways to improve:</p><ul><li><p>Use <code>ReentrantLock</code> (more flexible)</p></li><li><p>Avoid holding locks while waiting (I/O, sleep, &#8230;)</p></li><li><p>Redesign to reduce shared state</p></li></ul><p>It is not that <code>synchronized</code> is forbidden, but you should not let the virtual thread remain tightly held while waiting.</p><h3>Do not pool virtual threads</h3><p>Another important principle is: <strong>do not pool virtual threads</strong>. This is a habit that is very easy to bring over from traditional threads &#8212; and it is wrong.</p><p>Pooling makes sense when threads are <strong>expensive resources</strong>, but <strong>virtual threads are not expensive in that way</strong>. The better model is to create one virtual thread per task, then let the JVM handle their execution.</p><p>In other words, if your task is the unit of work, the virtual thread should be treated as an <strong>abstraction</strong> for that task, not as a precious worker that must be kept for reuse.</p><h3>Be careful with <code>ThreadLocal</code></h3><p>With platform threads, <code>ThreadLocal</code> is sometimes a convenient way to attach data to a thread. But with virtual threads, the number of threads can be very large, so if you abuse <code>ThreadLocal</code>, you can accidentally increase memory usage quickly and make the code harder to control.</p><p>If your goal is to limit access to a finite resource such as a database connection, a semaphore is often clearer and more direct.</p><h3>Do not combine with parallel streams</h3><p>A common misunderstanding is:</p><blockquote><p><strong><s>Virtual thread + parallel stream = faster</s></strong></p></blockquote><p>Parallel streams are mainly designed for <strong>CPU-bound</strong> <strong>workloads</strong> and usually rely on <code>ForkJoinPool</code>, while virtual threads are strongest for <strong>I/O-bound</strong> <strong>workloads</strong> where most of the time is spent waiting.</p><p>The two do not conflict, but they do not naturally amplify each other either. Unless there is a clear reason, combining them usually does not improve performance and can even make the system less predictable.</p><h2>Conclusion</h2><p>In short, virtual threads do not replace every concurrency model, but they are a major step forward for Java in keeping code readable while still scaling well. Used in the right place, they reduce complexity without forcing developers into a heavy asynchronous architecture.</p><blockquote><p><strong>If you&#8217;re using Java 21 or later, virtual threads are officially ready to use. If you&#8217;re still using an older version, are you ready to upgrade your Java version ? :D :D</strong>  </p></blockquote><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Building an "All-in-One" Monitoring System with OpenTelemetry and SigNoz]]></title><description><![CDATA[From manual SSH debugging to total system visibility&#8212;how to implement a unified monitoring stack that scales with your code]]></description><link>https://quangchientran.substack.com/p/building-monitoring-system-opentelemetry-signoz</link><guid isPermaLink="false">https://quangchientran.substack.com/p/building-monitoring-system-opentelemetry-signoz</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Sun, 19 Apr 2026 15:13:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2bEL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After my first year as a backend engineer, the thing I&#8217;m most proud of isn&#8217;t a difficult feature, but successfully <strong>building a monitoring system from scratch</strong>.</p><p>It helped me realize a harsh truth: <strong>understanding the code is not enough</strong>. Only when I could see a request moving through each service did I truly understand how the system actually works. For the first time, I learned that debugging is not about guessing, but about <strong>observing</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2bEL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2bEL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 424w, https://substackcdn.com/image/fetch/$s_!2bEL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 848w, https://substackcdn.com/image/fetch/$s_!2bEL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 1272w, https://substackcdn.com/image/fetch/$s_!2bEL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2bEL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp" width="1456" height="724" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:724,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;SigNoz dashboard with application performance metrics - APM&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SigNoz dashboard with application performance metrics - APM" title="SigNoz dashboard with application performance metrics - APM" srcset="https://substackcdn.com/image/fetch/$s_!2bEL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 424w, https://substackcdn.com/image/fetch/$s_!2bEL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 848w, https://substackcdn.com/image/fetch/$s_!2bEL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 1272w, https://substackcdn.com/image/fetch/$s_!2bEL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2f103078-79c5-4c53-9339-09ea72a31628_2400x1194.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"></div></div></a></figure></div><h2>Why I had to build a Monitoring platform</h2><p>At that time, our system barely had a proper Monitoring platform. Whenever an incident happened, my process was roughly: open Terminal, SSH into each server, then use <code>grep</code> and <code>tail -f</code> to search through endless log files.</p><p>Debugging back then was basically like searching for a needle in a haystack, moving through a sea of logs and switching back and forth between a bunch of services.</p><ul><li><p><strong>Lost direction:</strong> I didn&#8217;t know which service the error started from in a whole forest of microservices.</p></li><li><p><strong>The trace was broken</strong>: A request might pass through 4 or 5 services, but I had no way to connect them together. It was like trying to find a person in a crowd without a photo.</p></li><li><p><strong>Deadline pressure:</strong> each debugging session could take hours, even days, while the whole team stayed anxious, and the bug was still there.</p></li></ul><p>What made me think was: &#8220;<em><strong>I wonder how many companies out there are still running like this?</strong></em>&#8221;</p><p>At my previous company, I was lucky to have access to Datadog. I have to admit, it was amazing. Everything felt modern, the UI was intuitive, and logs, metrics, and traces were all delivered in one place.</p><p>But ironically, at that time I only used Datadog at a surface level. I knew it was convenient for checking logs faster than SSH, but I hadn&#8217;t truly understood the core value of observability. Only after I no longer had it did I realize how much I was missing.</p><p>And that made me think: &#8220;<em><strong>Why don&#8217;t I build a system like this myself? A solution that gives real visibility into my system?</strong></em>&#8221;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0WMl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0WMl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 424w, https://substackcdn.com/image/fetch/$s_!0WMl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 848w, https://substackcdn.com/image/fetch/$s_!0WMl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!0WMl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0WMl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg" width="1200" height="673" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:673,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0WMl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 424w, https://substackcdn.com/image/fetch/$s_!0WMl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 848w, https://substackcdn.com/image/fetch/$s_!0WMl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!0WMl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee99640a-8ab4-4366-a5db-121a02f3a667_1200x673.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h2>It wasn&#8217;t as easy as I imagined</h2><p>In my head, I started looking for ways to build an Monitoring system like that. My criteria at the beginning were simple.</p><p><strong>Simplicity</strong>: as someone who wasn&#8217;t originally deep into DevOps, I needed something that would work after installation. Making it complicated from the start was the fastest way to quit.</p><p><strong>Minimal code changes</strong>: the system was already running stably. Touching the application logic just to add Monitoring was a huge risk. I wanted to &#8220;<strong>add on,</strong>&#8221; not &#8220;<strong>rewrite</strong>.&#8221;</p><p><strong>Minimal tooling</strong>: the more tools you have, the more failure points you create. I didn&#8217;t want to spend my whole day maintaining Monitoring tools.</p><p><strong>Open source</strong>: not just because I wanted something &#8220;<strong>cheaper&#8221;</strong>, but because I wanted a strong community and full control over the data without depending on a vendor&#8217;s pricing.</p><p>But in reality, no solution is perfect from the beginning.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UqwB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UqwB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!UqwB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!UqwB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!UqwB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UqwB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bb82702e-1732-4d19-ba14-929be4047383_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UqwB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!UqwB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!UqwB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!UqwB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb82702e-1732-4d19-ba14-929be4047383_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h3>SaaS: Datadog, New Relic</h3><p>I have to admit, these are nearly perfect solutions. But the price to pay is&#8230; too <strong>expensive</strong>.</p><p>For a small startup, <strong>spending several thousand USD per month just to view logs feels like an unnecessary luxury</strong>. I wanted something that could give me the Datadog experience, but at the cost of self-hosting.</p><h3>Grafana Tempo, Grafana Loki, Prometheus, InfluxDB, Kibana, Grafana</h3><p>This is a truly destructive combo in the open-source world. It&#8217;s complete and powerful, <strong>but extremely fragmented.</strong></p><p>The <strong>configuration nightmare</strong> is real: you have to learn Prometheus for metrics, Loki for logs, and Tempo for tracing.</p><p>The pieces are disconnected, and making them &#8220;<strong>understand</strong>&#8221; <strong>each other is a nightmare</strong>. I tried it and quickly realized that I wanted to be a developer building products, not a <strong>full-time Monitoring engineer</strong> just to maintain this stack.</p><h3>AWS CloudWatch, X-Ray, Trace</h3><p>Even though the system was running on AWS infrastructure, honestly I just couldn&#8217;t get used to CloudWatch&#8217;s interface. It felt old, fragmented, and the user experience wasn&#8217;t smooth.</p><blockquote><p><em><strong>Sorry AWS. I still love you.</strong></em></p></blockquote><h2>The solution appeared unexpectedly</h2><p>This time, I didn&#8217;t find it by reading blogs. I attended a tech conference called <strong>Devoxx</strong>, where I discovered a lot of interesting topics.</p><p>By chance, I joined a talk from a movie streaming company. They introduced exactly the combination I had been looking for: <strong>OpenTelemetry + SigNoz</strong>.</p><p>My reaction at the time was basically: &#8220;<em><strong>Wow, this is good</strong></em>.&#8221;</p><h3>OpenTelemetry: the common language of distributed systems</h3><p>If every service in your system speaks a different Monitoring language, then understanding the full picture is impossible. OpenTelemetry was created to solve that problem.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DRw7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DRw7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!DRw7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!DRw7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!DRw7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DRw7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DRw7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!DRw7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!DRw7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!DRw7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ec8355-9c0e-49f6-817e-e81337c7a95c_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>It provides a powerful abstraction: <strong>OTel is not a place where data is stored, but a standard framework for generating and collecting observability data</strong>. It acts like a <strong>translator</strong>, turning signals from <strong>logs, metrics, and traces into a common format</strong>.</p><p>It is also highly flexible. This is its core value. With OTel, you are no longer locked into Datadog or New Relic. You can <strong>switch backends simply by changing the data routing configuration, without changing a single line of business logic</strong>.</p><p>It also gives you end-to-end tracing. OTel lets you attach a <strong>Trace ID</strong> to each request from the moment it enters the system. From there, you can follow its journey <strong>across dozens of microservices</strong> and record every part of that path.</p><h3>SigNoz: all in one</h3><p>If OpenTelemetry is the data collector, then SigNoz is the place where that data gets turned into <strong>valuable insights</strong>. Everything you need for a Monitoring system is in SigNoz already.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GMtl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GMtl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 424w, https://substackcdn.com/image/fetch/$s_!GMtl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 848w, https://substackcdn.com/image/fetch/$s_!GMtl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 1272w, https://substackcdn.com/image/fetch/$s_!GMtl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GMtl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp" width="1456" height="844" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:844,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Enterprise observability hero&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Enterprise observability hero" title="Enterprise observability hero" srcset="https://substackcdn.com/image/fetch/$s_!GMtl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 424w, https://substackcdn.com/image/fetch/$s_!GMtl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 848w, https://substackcdn.com/image/fetch/$s_!GMtl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 1272w, https://substackcdn.com/image/fetch/$s_!GMtl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6dd7779-f583-46d4-beca-05afd6df1aa6_2400x1391.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>It centralizes everything. Instead of jumping between Prometheus for metrics, Loki for logs, and Jaeger for traces, SigNoz brings everything into a single interface. That correlation is extremely important: <strong>when you see a metric spike, you can immediately click into it and inspect the related logs and traces at the exact same time</strong>.</p><p>It also integrates deeply with OpenTelemetry. SigNoz is built on OTel from day one. It doesn&#8217;t just display data, but also provides advanced features such as <strong>filtering traces by latency, analyzing exceptions, and setting up intelligent alerts</strong>.</p><p>In terms of performance and cost, SigNoz is written in <strong>Go</strong> and uses <strong>ClickHouse</strong>, a very fast columnar database, which allows it to process billions of records with much lower operating cost than traditional SaaS solutions. You also have full control over your data, which is a key factor for businesses that care about security.</p><h3>The OpenTelemetry + SigNoz combo</h3><p>Imagine your microservices system as a skyscraper. OpenTelemetry is the international-standard wiring and power sockets installed in every room. SigNoz is the giant 8K TV plugged into that system to display all the security camera feeds.</p><p>What&#8217;s great about this is that if you later want to switch to another &#8220;<strong>TV</strong>&#8221; like Datadog or Jaeger, you just unplug the old one and plug in the new one. You don&#8217;t need to drill through walls and redo all the wiring, meaning you don&#8217;t need to rewrite the code from scratch. That freedom is the biggest value OpenTelemetry brings.</p><p>Without the connection between logs and metrics, you can end up in a situation where metrics show CPU jumping to 95%, but when you open the logs you see thousands of lines flowing every second. You start panicking: &#8220;<strong>Where is the error in this mess?</strong>&#8221;</p><p>With SigNoz, logs are no longer isolated. A log line showing a <code>500 Internal Server </code>Error now comes with full context:</p><ul><li><p>Which <strong>Trace ID</strong> does it belong to? That is, the request journey.</p></li><li><p>What were the <strong>CPU and RAM usage</strong> of that service at the time?</p></li><li><p>What was the <strong>latency at the database</strong> call step?</p></li></ul><p>When all the data can talk to each other, debugging is no longer guesswork. It becomes an investigation based on <strong>real evidence</strong>.</p><h2>Getting started right away</h2><p>After coming back, I jumped straight into it, just to keep the momentum going, haha. Contrary to my early concerns that the system would be too complex, the real integration process turned out to be surprisingly smooth. I split the roadmap into 3 steps.</p><ul><li><p><strong>Normalize Logs:</strong> I standardized logs by converting Spring Boot logs into JSON using Logback, which is a built-in feature in Spring Boot.</p></li><li><p><strong>Deploy SigNoz:</strong> through Docker &#8212; fast, clean, and without needing much configuration effort.</p></li><li><p><strong>Activate OpenTelemetry:</strong> by running the OpenTelemetry Agent together with the Java application. I added it in the Dockerfile like this below. This was the most magical step: without touching a single line of business logic, data started flowing into SigNoz immediately.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;20139157-2c48-46cf-af27-959827697804&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">ENTRYPOINT [&#8221;java&#8221;, &#8220;-javaagent:/opentelemetry-javaagent.jar&#8221;, &#8220;-Dotel.exporter.otlp.protocol=grpc&#8221;, &#8220;-jar&#8221;, &#8220;/myapp.jar&#8221;]</code></pre></div><p>Everything started working almost immediately. It only took me <strong>2 weeks</strong> to get the first version into production.</p><p>For the first time in many years of operating the system, the whole team could:</p><ul><li><p><strong>See traces</strong> and visually understand how a request moved through services.</p></li><li><p><strong>Identify bottlenecks</strong> and track exactly which service was slowing down the whole system, without guessing p99 from intuition anymore.</p></li><li><p><strong>Debugging</strong> faster, cutting the time to find an issue from hours down to just a few minutes.</p></li></ul><h2>What I learned from Monitoring</h2><p>While working with Monitoring, I discovered a lot of concepts that I hadn&#8217;t truly understood before.</p><h3>Observability has three pillars.</h3><ul><li><p><strong>Logs:</strong> are timestamped event records that capture individual events such as errors, warnings, and informational messages. They are used to debug specific incidents and understand what happened and why.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q-Ra!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 424w, https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 848w, https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 1272w, https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg" width="1200" height="597" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:597,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Log management hero&quot;,&quot;title&quot;:&quot;Log management hero&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Log management hero" title="Log management hero" srcset="https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 424w, https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 848w, https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 1272w, https://substackcdn.com/image/fetch/$s_!Q-Ra!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9532d35e-d9b5-43d6-8f43-9b3e3bd7a4f1_1200x597.svg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div></li><li><p><strong>Metrics: </strong> are numeric time-series data such as counters, gauges, and histograms. They show trends and resource usage over time, such as CPU, memory, request rate, error rate, and latency percentiles. They are used for dashboards, alerting, and long-term trend analysis.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PylJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PylJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 424w, https://substackcdn.com/image/fetch/$s_!PylJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 848w, https://substackcdn.com/image/fetch/$s_!PylJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 1272w, https://substackcdn.com/image/fetch/$s_!PylJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PylJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp" width="1456" height="859" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:859,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Metrics Explorer overview interface showing comprehensive metrics visibility&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Metrics Explorer overview interface showing comprehensive metrics visibility" title="Metrics Explorer overview interface showing comprehensive metrics visibility" srcset="https://substackcdn.com/image/fetch/$s_!PylJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 424w, https://substackcdn.com/image/fetch/$s_!PylJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 848w, https://substackcdn.com/image/fetch/$s_!PylJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 1272w, https://substackcdn.com/image/fetch/$s_!PylJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2656370f-986c-44ea-a651-39a429ade6f1_2730x1610.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div></li><li><p><strong>Traces:</strong> are an end-to-end view of a request&#8217;s journey as it passes through multiple services, including spans and a trace ID. They are extremely useful for identifying where latency or errors are introduced in microservice systems.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SgOt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SgOt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 424w, https://substackcdn.com/image/fetch/$s_!SgOt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 848w, https://substackcdn.com/image/fetch/$s_!SgOt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 1272w, https://substackcdn.com/image/fetch/$s_!SgOt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SgOt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png" width="1456" height="903" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:903,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Traces Explorer&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Traces Explorer" title="Traces Explorer" srcset="https://substackcdn.com/image/fetch/$s_!SgOt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 424w, https://substackcdn.com/image/fetch/$s_!SgOt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 848w, https://substackcdn.com/image/fetch/$s_!SgOt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 1272w, https://substackcdn.com/image/fetch/$s_!SgOt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d2b5c25-b875-481c-b912-f9d2e77befa9_2880x1786.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div></li></ul><h3>There are also related concepts.</h3><ul><li><p><strong>SLA (Service Level Agreement)</strong>: is like a contract with the customer. If it is violated, there may be penalties or compensation.</p></li><li><p><strong>SLO (Service Level Objective)</strong>: is the internal reliability target set by the team, and it is often stricter than the SLA.</p></li><li><p><strong>SLI (Service Level Indicator)</strong>: is the actual measured signal used to determine whether the SLO is being met.</p></li></ul><h3>Latency percentiles are also important.</h3><ul><li><p><strong>p50</strong>: half of users experience this speed or faster. It is the median value.</p></li><li><p><strong>p95</strong>: only 5% of users are slower than this. If your p95 is too high, it means 1 out of every 20 users is experiencing pain.</p></li><li><p><strong>p99</strong>: 99% of requests are faster than this, and only 1% are slower.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dbqn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dbqn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 424w, https://substackcdn.com/image/fetch/$s_!dbqn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 848w, https://substackcdn.com/image/fetch/$s_!dbqn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 1272w, https://substackcdn.com/image/fetch/$s_!dbqn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dbqn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp" width="1430" height="789" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:789,&quot;width&quot;:1430,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;SigNoz UI showing the Services section&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="SigNoz UI showing the Services section" title="SigNoz UI showing the Services section" srcset="https://substackcdn.com/image/fetch/$s_!dbqn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 424w, https://substackcdn.com/image/fetch/$s_!dbqn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 848w, https://substackcdn.com/image/fetch/$s_!dbqn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 1272w, https://substackcdn.com/image/fetch/$s_!dbqn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03aac140-8efe-4029-bd88-c855984f035c_1430x789.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>Why should we pay special attention to <strong>p99</strong>? Imagine a modern website has to call 100 microservices to finish rendering the homepage. If each service has a p99 of 1 second, the probability that at least one service becomes slow, and therefore slows down the whole page, becomes extremely high. In distributed systems, <strong>p99 is not the exception &#8212; it is the future of your system if you do not control it well</strong>. I explained this in depth in a dedicated article about latency, so feel free to read more there.</p><h3>Tracing: the connecting thread</h3><p>If metrics tell you that the system has high latency, <strong>tracing</strong> tells you exactly where the bottleneck is.</p><p>When looking at a trace in SigNoz, the request journey is no longer a black box:</p><div class="callout-block" data-callout="true"><p><em>Client &#8594; API Gateway &#8594; Order Service &#8594; Payment Service &#8594; Database</em></p></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KNtl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KNtl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 424w, https://substackcdn.com/image/fetch/$s_!KNtl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 848w, https://substackcdn.com/image/fetch/$s_!KNtl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 1272w, https://substackcdn.com/image/fetch/$s_!KNtl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KNtl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp" width="1430" height="743" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:743,&quot;width&quot;:1430,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Trace Details Interface&quot;,&quot;title&quot;:&quot;Trace Details Interface&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Trace Details Interface" title="Trace Details Interface" srcset="https://substackcdn.com/image/fetch/$s_!KNtl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 424w, https://substackcdn.com/image/fetch/$s_!KNtl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 848w, https://substackcdn.com/image/fetch/$s_!KNtl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 1272w, https://substackcdn.com/image/fetch/$s_!KNtl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4eb390b0-e5d4-4872-87f4-d63411d82149_1430x743.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>Everything is shown in a clear Gantt-style visualization.</p><ul><li><p><strong>Service involvement</strong>: Does Service A calling Service B result in an error right at the gateway?</p></li><li><p><strong>Where is the bottleneck?</strong> Does the database call take 800ms while the entire request takes only 1 second? You immediately know you need to optimize the query instead of fixing the Java code.</p></li><li><p><strong>Asymmetry</strong>: Some requests take 10 steps but are extremely fast, while others take only 2 steps but are extremely slow. Tracing helps you identify these bottlenecks.</p></li></ul><p>Even now, I still haven&#8217;t explored every corner of SigNoz, but one thing is certain: <strong>my mindset has changed.</strong></p><p>Instead of SSH-ing into servers and digging through lines of logs, I can now look directly at charts and observe the full flow of data. This tool didn&#8217;t just save me from bugs that came from nowhere, it also made me more confident when designing larger systems, because I know I have the ability to control them.</p><h2>End</h2><p>Looking back on that journey, I realized that Monitoring is not just about tools, but about understanding.</p><p>To be honest, I&#8217;m not yet a true monitoring expert, but SigNoz and OpenTelemetry helped me understand the system a hundred times better than just sitting there grepping logs like I used to.</p><blockquote><p><em><strong>Don&#8217;t wait until your system crashes to build Monitoring. Build it while the system is still running well, so you know what &#8220;good&#8221; looks like before you learn what &#8220;bad&#8221; feels like.</strong></em></p></blockquote><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Deep Inside PostgreSQL: Processes, Forking, and the Memory Trade-off]]></title><description><![CDATA[Understanding PostgreSQL Architecture: Process-per-Connection, Resource Management, and Scaling with Connection Pooling]]></description><link>https://quangchientran.substack.com/p/deep-inside-postgres-processes-forking</link><guid isPermaLink="false">https://quangchientran.substack.com/p/deep-inside-postgres-processes-forking</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:36:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Mts_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>PostgreSQL is one of the most widely used relational databases today. I&#8217;ve used it myself and have grown to really like it. It is an open-source platform that is continuously updated, and the latest version, as of now, is version 18, I believe. PostgreSQL was created to handle <strong>high concurrency across many read, write, and update workloads</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Mts_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Mts_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!Mts_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!Mts_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!Mts_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Mts_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png" width="1408" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/77d3d150-9374-454e-8704-fceb198e799c_1408x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2291041,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.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_!Mts_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!Mts_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!Mts_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!Mts_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77d3d150-9374-454e-8704-fceb198e799c_1408x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"></div></div></a></figure></div><p>In this article, I won&#8217;t go over PostgreSQL&#8217;s old or new features, since those are easy to find with a quick search. Instead, I want to go deeper into the principles and internal architecture behind PostgreSQL &#8212; the things that affect performance and the challenges PostgreSQL has to deal with and solve.</p><p>There are two basic principles in PostgreSQL: <strong>process per connection</strong> and <strong>copy-on-write (MVCC &#8212; Multi-Version Concurrency Control)</strong>. We already covered <a href="https://open.substack.com/pub/quangchientran/p/10-years-as-a-developer-but-only?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">MVCC in a previous article</a>. In this one, we&#8217;ll focus on the <strong>process per connection</strong> principle, which anyone learning PostgreSQL should understand clearly. Let&#8217;s get started.</p><h2><strong>Process per connection</strong></h2><p>This is the core principle of PostgreSQL: each connection to the database becomes a new process. That is different from MySQL, is commonly described as using a thread-based model for connections.</p><p>The main components are:</p><ul><li><p><strong>Postmaster</strong>: the main server process that listens for new client connections and starts backend processes</p></li><li><p><strong>Backend process</strong>: forks a new process for each connection.</p></li><li><p><strong>Shared memory</strong>: stores the buffer cache and locks shared by all processes.</p></li><li><p><strong>Background processes</strong>: run in the background for automatic tasks such as autovacuum, checkpointer, and others.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Lt7M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Lt7M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!Lt7M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!Lt7M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!Lt7M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Lt7M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:557957,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.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_!Lt7M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!Lt7M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!Lt7M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!Lt7M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dcba101-a7e6-41ec-84b1-0ae146f0fae9_1024x559.png 1456w" sizes="100vw"></picture><div class="image-link-expand"></div></div></a></figure></div><h2><strong>The lifecycle of a request</strong></h2><p>When we run this query:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;251db20f-1c4d-410e-afc8-c06d0d3b4914&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT * FROM users WHERE id = 1;</code></pre></div><p>The client sends a TCP/IP request to the PostgreSQL server to execute that statement through the standard 3-way handshake:</p><ul><li><p><strong>SYN</strong>: the client sends a packet &#8212; &#8220;Hello, server.&#8221;</p></li><li><p><strong>SYN-ACK</strong>: the server responds &#8212; &#8220;Hello, client.&#8221;</p></li><li><p><strong>ACK</strong>: the client confirms the connection.</p></li></ul><p>After receiving the connection request, the postmaster forks a separate backend process that has nothing to do with other connections.</p><p>That backend process handles the statement in three steps:</p><ul><li><p><strong>Parse</strong>: checks the query syntax and builds a parse tree.</p></li><li><p><strong>Planner</strong>: creates the execution plan and decides things like index scan, full table scan, and join strategy.</p></li><li><p><strong>Executor</strong>: runs the query using the execution plan and accesses data through PostgreSQL&#8217;s buffer manager. If the needed data is not in memory, it may trigger disk I/O.</p></li></ul><p>Finally, the backend process sends the result back to the client through the socket.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eJ3f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eJ3f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!eJ3f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!eJ3f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!eJ3f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eJ3f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:392789,&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://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.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_!eJ3f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!eJ3f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!eJ3f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!eJ3f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce36c554-dd3e-4c61-8025-4cf21f984611_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h2><strong>Advantages of process per connection</strong></h2><h2><strong>Isolation</strong></h2><p>Each PostgreSQL connection is handled by a separate process, so one slow connection or one connection consuming a lot of CPU will not directly affect the others. A simple way to think about it is that each customer has their own support agent. If one customer is difficult or takes a lot of time, only that agent is affected, not the other customers.</p><p>Each backend process has its own workspace, its own memory, and its own execution flow at the operating-system level. <strong>So if one heavy query consumes 80% of the CPU, it mainly slows down that process itself instead of interfering with other connections</strong>.</p><p>In thread-based database systems such as MySQL, many connections <strong>share one larger process and the resource</strong>s inside it. When one thread becomes overloaded, CPU scheduling, lock contention, or shared resource bottlenecks can make the other threads wait longer. That means average latency can rise under heavy load, especially when many connections are active at the same time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Yxuk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Yxuk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!Yxuk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!Yxuk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!Yxuk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Yxuk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:482487,&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://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.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_!Yxuk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!Yxuk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!Yxuk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!Yxuk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13b43abd-e86c-4d70-be36-37ada30f1fcf_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h2><strong>Security</strong></h2><p>With PostgreSQL, each connection is handled separately. That means when a user logs in, the system checks authentication, access rights, and session state for each connection individually instead of putting everything into one shared place.</p><p>This improves security because if one connection has a problem, it only affects that session and does not easily spread to other connections.</p><p>A simple analogy is an apartment building where each person has their own room and their own lock. If one room has a problem, the others are fine.</p><p>In thread-based systems, many connections may share more common resources. When too many requests arrive at once, that sharing turns into resource contention. If something goes wrong in the shared part, it can affect more connections.</p><h2><strong>Scalability</strong></h2><p>PostgreSQL can scale to serve many concurrent users while still maintaining fairly stable performance if it is configured properly. Because it uses process per connection:</p><ul><li><p>Each connection has its own processing space.</p></li><li><p>One heavy connection does not immediately block the whole system.</p></li><li><p>Each session can be isolated more easily.</p></li></ul><p>However, <strong>each process also consumes resources such as CPU and memory</strong>. As the number of connections grows too large, resource cost grows with it. So while PostgreSQL scales well, that does not mean performance keeps improving forever just because more connections are added.</p><h2><strong>Flexibility</strong></h2><p>PostgreSQL supports several types of connections:</p><ul><li><p><strong>TCP/IP</strong>: usually used when the application and database are on different machines or networks. This is the most common connection type because it is flexible and easy to use in production, cloud, and microservices environments. The downside is network overhead, so it is usually slower than local connections.</p></li><li><p><strong>Unix socket</strong>: usually used when the application and database are on the same server. Because it does not go through the TCP/IP stack, it has lower latency and uses fewer resources.</p></li><li><p><strong>Shared memory</strong>: Shared Memory is an <strong>Internal IPC (Inter-Process Communication)</strong> mechanism used by PostgreSQL to share buffer cache, locks, and other internal state across backend processes. This is extremely fast because data is exchanged directly through memory without going over the network.</p></li></ul><h2><strong>The trade-off</strong></h2><p>Because PostgreSQL uses process per connection, each connection creates a separate process. That means each connection is not just a session &#8212; it also requires operating-system resources to run independently.</p><p>The downside is that when the <strong>number of connections grows, resource usage grows quickly</strong> too.</p><h2><strong>What is </strong><code>fork()</code><strong>?</strong></h2><p><code>fork()</code> is an operating-system call that creates a child process from a parent process. At first, the child process is almost identical to the parent, and then PostgreSQL separates it to serve one specific connection.</p><p>When <strong>PostgreSQL</strong> forks a process, the OS doesn't actually copy all the memory. It uses <strong>Copy-on-Write (CoW)</strong>. The new process "points" to the parent's memory. Only when the new process tries to <em>change</em> something does the OS actually copy that specific page of memory. This is why <strong>PostgreSQL</strong> can start processes relatively quickly, though still slower than threads.</p><p>When a new process is created, the operating system must prepare:</p><ul><li><p>memory for the process</p></li><li><p>file descriptors</p></li><li><p>related system state</p></li><li><p>process management information</p></li></ul><p>Even though PostgreSQL uses optimizations like copy-on-write, creating a separate process still costs much more than creating a thread. That is why each PostgreSQL connection has its own overhead.</p><p><strong>Overhead</strong> is the cost of operating a process that is not directly part of handling the client&#8217;s request.</p><p>The amount of memory each process uses depends on:</p><ul><li><p>PostgreSQL version</p></li><li><p>server configuration</p></li><li><p>workload</p></li><li><p>extensions</p></li><li><p>operating system</p></li></ul><h2><strong>CPU context switching</strong></h2><p>When the CPU switches from one process or thread to another, it must save the current state and load the new one. That state includes registers, the instruction pointer, and part of the current execution information.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qbGa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qbGa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!qbGa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!qbGa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!qbGa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qbGa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:640125,&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://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.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_!qbGa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!qbGa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!qbGa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!qbGa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5bb64b-9367-44ab-beb5-abfbf07d424d_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>This switching is called <strong>context switching</strong>. For example, with two processes A and B, it happens when process A becomes <strong>blocked</strong> &#8212; for example, <strong>waiting for I/O</strong> &#8212; or when it has used up its <strong>time slice</strong>. There are three common situations:</p><ul><li><p>Process A is running but must wait for I/O, such as disk access, network access, or incoming data. In that case, Process A moves to the waiting/blocked state.</p></li><li><p>Process A uses up its allocated CPU time, and the operating system stops it to give CPU time to another process.</p></li><li><p>A process with higher priority appears, so the scheduler decides to switch context.</p></li></ul><p>If a PostgreSQL server has only 8 CPU cores but thousands of database connections arrive at the same time, the operating system cannot run them all simultaneously. It must divide CPU time into tiny slices for each connection. So the pattern becomes:</p><ul><li><p>run process A for a short time</p></li><li><p>stop</p></li><li><p>save A&#8217;s state</p></li><li><p>switch to process B</p></li><li><p>repeat for all other connections</p></li></ul><p>Each switch takes time. The CPU is not only doing useful work &#8212; it is also spending time moving between tasks. When the number of connections is too large, <strong>the CPU spends too much time switching instead of processing queries</strong>, and performance drops.</p><p>A simple analogy is a chef with only one pan who has to cook for 100 customers. If the chef keeps jumping back and forth between dishes, a lot of time is wasted switching instead of finishing one dish at a time. The CPU behaves in a similar way.</p><h2><strong>Disk contention</strong></h2><p>Disk contention means many requests competing to read and write disk at the same time. Databases depend heavily on disk speed because data is not always in RAM. When cache is not enough, or the data is not already in memory, the system has to go to disk.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CZIY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CZIY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!CZIY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!CZIY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!CZIY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CZIY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:643491,&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://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.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_!CZIY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!CZIY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!CZIY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!CZIY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f2c995e-08e5-463b-a8c0-a69bfa6a68a0_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><p>When too many requests arrive at once, the disk gets congested because it must continuously serve many different requests.</p><p>If the system only handles a few requests, it can read data in a mostly continuous way, called <strong>sequential read</strong>. This is fast because the disk reads nearby blocks in one pass.</p><p>But when hundreds of requests arrive at the same time, each one may need data from a different location. Then the disk has to jump around a lot, which becomes <strong>random I/O</strong>, which is much slower than sequential access.</p><p>Think of a librarian: if only a few people ask for books near each other, the librarian only needs to walk to one shelf area. But if each person asks for books in different parts of the library, the librarian has to move all over the place, which takes much more time.</p><h2><strong>Connection pooling</strong></h2><p>With PostgreSQL, connection pooling is often <strong>essential</strong> when a system has many requests or many application instances. The core reason is that PostgreSQL uses the process-per-connection model, so if your app opens and closes connections continuously, the database spends a lot of effort <strong>creating, managing, and cleaning up connections instead of doing useful work like executing queries</strong>.</p><p>The problem is that every time the app opens a new connection, PostgreSQL has to perform the handshake, create a backend process, allocate resources, and later release everything when the statement finishes. This creates <strong>connection churn</strong> &#8212; connections coming and going all the time &#8212; which is expensive in CPU, RAM, and latency.</p><p>If the number of connections gets too high, you also run into more context switching, higher memory pressure, and longer processing queues. This is especially bad when traffic spikes suddenly or when there are many <strong>short-lived requests</strong>, because the database ends up spending a lot of effort on &#8220;<strong>connections</strong>&#8221; instead of &#8220;<strong>queries</strong>.&#8221;</p><p>So PostgreSQL made a smart trade-off: &#8220;<em><strong>why keep creating and destroying expensive connections when we can create some in advance and reuse them</strong></em>&#8221;?</p><p>Connection pooling keeps a set of &#8220;<strong>warm</strong>&#8221; connections ready for reuse. When a request comes in, the app borrows a connection from the pool, runs the query, and returns it to the pool instead of creating a new one from scratch. This reduces handshake cost and the cost of creating new processes.</p><p>In other words, connection pooling <strong>changes the problem from &#8220;one request, one new connection&#8221; into &#8220;many requests sharing a smaller group of connections.&#8221;</strong> That improves throughput and reduces latency.</p><p>Because PostgreSQL does not use thread-per-connection, it gains isolation and stability, but the cost is that each connection is heavier. So when many requests arrive, resources can grow very quickly.</p><p>For PostgreSQL, <strong>connection pooling is not just an extra optimization</strong> &#8212; it is often what keeps the system healthy when there are many concurrent users or many application instances.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ps72!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ps72!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!Ps72!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!Ps72!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!Ps72!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ps72!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:783258,&quot;alt&quot;:&quot;&quot;,&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://quangchientran.substack.com/i/192788246?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!Ps72!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!Ps72!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!Ps72!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!Ps72!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3e57f4d-8bc6-45bb-8291-4c008b684ccf_1024x559.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h2><strong>When pooling matters most</strong></h2><p>Pooling is especially useful when the system has these characteristics:</p><ul><li><p>Many short requests, such as web APIs.</p></li><li><p>The application is scaled to many instances connecting to one shared database.</p></li><li><p>Traffic arrives in bursts.</p></li><li><p>The application is serverless or built with microservices, where connect/disconnect happens frequently.</p></li></ul><p>In those cases, PostgreSQL can become overwhelmed without pooling, even if the queries themselves are not very heavy.</p><p>One important thing: <strong>pooling does not make your SQL queries faster</strong>. If a query scans an entire table, lacks an index, or is blocked by a lock, pooling only <strong>helps you avoid connection overhead</strong> &#8212; it does not replace SQL optimization.</p><p>In other words, pooling solves connection cost, while indexes, query plans, and schema design solve query cost.</p><h2><strong>How to estimate pool size</strong></h2><p>For PostgreSQL, the pool size should satisfy three conditions:</p><ul><li><p>It should not exceed the number of processes the DB and CPU can handle.</p></li><li><p>It should not exhaust RAM, because each process/connection uses its own memory.</p></li><li><p>It should not make queries wait too long in line.</p></li></ul><p>There is no perfect formula, but a practical starting point is:</p><pre><code><strong>pool_size</strong> = min(
<strong>cores</strong> <strong>* 2</strong>, 
<strong>floor</strong>(<strong>RAM_for_pool_MB</strong> / <strong>memory_per_connection_MB</strong>), 
<strong>max_db_connections</strong> - <strong>reserve
</strong>)</code></pre><p>Where:</p><ul><li><p><strong>RAM_for_pool_MB</strong> is the memory reserved specifically for connections, not the entire server RAM.</p></li><li><p><strong>memory_per_connection_MB</strong> is the memory used by one connection process, and it should ideally be measured in practice rather than guessed.</p></li><li><p><strong>reserve</strong> should keep about 5 to 20 connections available for admin, monitoring, and maintenance.</p></li></ul><p>For example, if your database has:</p><ul><li><p>4 cores</p></li><li><p>8 GB RAM, but only 1 GB reserved for connections</p></li><li><p>each connection process uses 10 MB</p></li><li><p>10 connections reserved for admin</p></li></ul><p>then:</p><pre><code><strong>pool_size = min(4 * 2, floor(1024 / 10), 100 - 10) = 8</strong></code></pre><p>This is only a starting configuration. You should still run load tests and monitor real behavior, because the best pool size depends on the workload.</p><h2><strong>Two kinds of pooling</strong></h2><p>There are two common types of pooling: <strong>application-side pooling</strong> and <strong>PgBouncer</strong>. Application-side pooling keeps connections inside each app instance, while PgBouncer sits between the app and PostgreSQL to collect many clients and limit the number of real backend connections reaching the database.</p><p>In simple terms:</p><ul><li><p>Application-side pool helps the application avoid reconnecting all the time.</p></li><li><p>PgBouncer helps the whole system avoid flooding the database with too many connections.</p></li></ul><h2><strong>Why application-side pooling alone is not enough</strong></h2><p>If you only have one application connected to the database, application-side pooling is often enough. But when you have many apps, multiple instances, autoscaling, or microservices, each instance creates its own pool. The total number of connections reaching PostgreSQL can add up very quickly and exceed what the database can comfortably handle.</p><p>For example:</p><ul><li><p>20 instances</p></li><li><p>10 connections per instance</p></li></ul><p>That already means 200 real connections to the database.</p><p>And <strong>PostgreSQL</strong> does not see 20 apps. It only sees 200 backend connections.</p><h2><strong>Why PgBouncer is useful</strong></h2><p><strong>PgBouncer</strong> acts like a lightweight connection proxy in front of PostgreSQL. It keeps a small number of real database connections and allows many clients to share them, especially in <strong>transaction pooling</strong> mode. This reduces process creation overhead, reduces memory usage, and helps prevent connection storms when traffic spikes.</p><p>A very practical benefit is that if your app has 1,000 logical clients but only needs 20 to 25 real backend connections, PostgreSQL will stay much healthier.</p><h2><strong>PgBouncer pooling modes</strong></h2><p>PgBouncer has 3 main modes:</p><ul><li><p><strong>Session pooling</strong>: a database connection is assigned to the client for the entire lifetime of the session. This is the simplest mode, but also the least efficient.</p></li><li><p><strong>Transaction pooling</strong>: a connection is assigned only during a transaction.</p></li><li><p><strong>Statement pooling</strong>: each query may use a different connection.</p></li></ul><p>For web workloads, transaction pooling is often the best choice because the connection is held only while the transaction is running, and then it is returned to the pool.</p><p>The key thing to remember is that statement pooling is powerful but does not work well with multi-statement transactions, while session pooling is safer but less efficient in terms of connection usage.</p><h2><strong>Conclusion</strong></h2><p>While PostgreSQL handles concurrency very well, performance can still degrade when the number of active connections becomes too high, especially because of context switching, memory pressure, and disk contention. That is why connection pooling is often essential in production systems.</p><p>Understanding PostgreSQL helps you debug better, understand the system more deeply, configure it correctly, and build applications that scale more safely and efficiently.</p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[The Latency Trap: Why Tips and Tricks Aren't Enough]]></title><description><![CDATA[Stop guessing why your requests are slow. Learn the fundamental formula: Latency = Propagation + Queueing + Service. Discover why p99 metrics matter and how to optimize your system without over-engine]]></description><link>https://quangchientran.substack.com/p/understanding-latency-a-simple-formula</link><guid isPermaLink="false">https://quangchientran.substack.com/p/understanding-latency-a-simple-formula</guid><pubDate>Tue, 24 Mar 2026 14:16:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!R4Dm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Up until now, through all my learning, thinking, and working, the concept of latency (request delay) has always been&#8230; kinda vague to me. Whenever I heard about it, in my head it was always:</p><blockquote><p><em><strong>&#8220;Ah okay, latency is the time from when the client sends a request until it receives the response. Done.&#8221;</strong></em></p></blockquote><p>Yeah&#8230; not wrong. Completely correct. But also&#8230; completely useless &#128516;. It&#8217;s one of those definitions that sounds obvious, like &#8220;yeah yeah everyone knows that&#8221;.<br>But when you actually start working with it, suddenly it explains&#8230; nothing. For example:</p><p>When a manager asks:</p><blockquote><p><em><strong>&#8220;Why is this request so slow? It just fetches a list, why does it take 2 seconds?&#8221;</strong></em></p></blockquote><p>Or:</p><blockquote><p><em><strong>&#8220;Why is the same request sometimes 200ms, sometimes 1s, sometimes 2s?&#8221;</strong></em></p></blockquote><p>And now you&#8217;re stuck. Because that &#8220;definition&#8221; doesn&#8217;t help you answer anything.</p><p>So&#8230; what actually causes latency?<br>Why is it fast sometimes and slow other times?<br>What exactly is happening inside a request?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R4Dm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R4Dm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!R4Dm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!R4Dm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!R4Dm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R4Dm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b84c1736-1deb-4265-8840-99fba7e44441_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1571034,&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://quangchientran.substack.com/i/191889458?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.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_!R4Dm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!R4Dm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!R4Dm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!R4Dm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb84c1736-1deb-4265-8840-99fba7e44441_1376x768.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h2><strong>Tips, Tricks, and Fancy Diagrams</strong></h2><p>If you search online for advice about design systems or how to optimize a request&#8217;s latency, you&#8217;ll find tons of methods &#8212; hundreds of architecture diagrams, and all sorts of fancy technical explanations and buzzwords. I used to do that too and thought:</p><blockquote><p><em><strong>&#8220;Wow, this makes perfect sense, let&#8217;s apply it!&#8221;</strong></em></p></blockquote><p>I also picked up quite a few latency-optimization skills, haha &#8212; if someone asked me about them in an interview, I&#8217;m ready &#128516;. For example:</p><ul><li><p>If too many messages hit the system at once and it gets overloaded, just push them all into a queue (like Kafka or SQS), then process them slowly, one by one &#8212; that way, no message gets lost.</p></li><li><p>When the system&#8217;s overloaded, scale it horizontally &#8212; spin up more instances, pods, or nodes to handle millions of requests. That&#8217;s what autoscaling with load balancers is for, right?</p></li><li><p>Bring the server closer to users with a CDN. With servers all around the world, European users reach the European server, Americans go to the US one &#8212; plus, that also helps reduce load on the main server.</p></li><li><p>Cache data with Redis &#8212; reading from RAM is so much faster than pulling from disk I/O, and it also takes some pressure off the database.</p></li><li><p>For databases, you can look into techniques like replication (to improve read performance), adding indexes, or using partitioning and sharding to make queries faster.</p></li></ul><p>And many more&#8230; And honestly? I agree with all of them.</p><p>But&#8230;</p><p>All those pieces of advice &#8212; to me, they&#8217;re just little tips and tricks. Maybe I remember them today and forget them tomorrow. There&#8217;s just too much going on in life to keep everything in my head.</p><p>I didn&#8217;t really understand the essence or the actual components of latency &#8212; what a request has to go through, what it faces, why it&#8217;s sometimes fast and sometimes slow. When it&#8217;s fast, <em>why</em> is it fast? When it&#8217;s slow, <em>why</em> is it slow?</p><p>I realized I understood nothing if I only relied on random tips and guesses.</p><h2><strong>The Formula That Changed Everything</strong></h2><p><strong>Thanks</strong></p><p>I want to thank a System Design Handbook shared recently by Quang Hoang. After reading it, I learned a lot of new things about latency. Not just tips I&#8217;ll forget later. The most valuable thing for me was this formula:</p><blockquote><p><strong>Latency = Propagation + Queueing + Service</strong></p></blockquote><p>Simple. Clean. Almost too simple. But this thing changed everything for me.</p><p>Latency has 3 components:</p><ul><li><p><strong>Propagation</strong> &#8594; time for the request to travel</p></li><li><p><strong>Queueing</strong> &#8594; time waiting (thread pool, DB connection pool, etc.)</p></li><li><p><strong>Service</strong> &#8594; time the server actually processes</p></li></ul><p>When I look at this formula&#8230; All the &#8220;tips &amp; tricks&#8221; suddenly become easy to understand. Because now:</p><blockquote><p><strong>Optimizing latency = optimizing these 3 things.</strong></p></blockquote><p>Examples:</p><ul><li><p>If the connection between the two sides (TCP handshakes, TLS handshakes, network, etc.) takes too long, then you should bring them closer together; if you can merge them or put them next to each other, even better.</p></li><li><p>If the queue is too long, you need to find a way to shorten it by reducing incoming load or increasing processing throughput (scaling servers) so the queue gets smaller.&#8203;</p></li><li><p>If the request handler itself is too slow, then you&#8217;d better optimize the algorithm, tune the database, or use a programming language that&#8217;s more suitable for the business problem, or find ways to process things in parallel and asynchronously.</p></li></ul><p>The more of these parts you can optimize, the better.</p><p>And if it&#8217;s still too complicated, then just hide that latency away and immediately return something like &#8220;processing completed&#8221; (succeeded) so the client feels at ease, even though behind the scenes the system is still working its butt off.</p><p>For example, when you create an AMI from an EC2 instance, AWS responds right away that the image creation is in progress, so the user can move on and do other things instead of staring at a loading spinner and waiting for the page to unblock.</p><p>Now I&#8217;ve developed a new habit: whenever I come across some tip or trick to optimize latency, I ask myself which of those three components it actually improves, what the trade-offs are, or whether it helps optimize all of them &#8212; instead of doing what I used to do: reading through long-winded explanations that I probably wouldn&#8217;t remember even for a day.</p><p>Maybe optimizing latency is still a huge topic, with plenty more going on behind the scenes, but for me, this formula already captures a big part of:</p><ul><li><p>The definition of latency</p></li><li><p>The components that make it up</p></li><li><p>The strategies to optimize it</p></li></ul><p>There are already tons of articles online about design systems and techniques for optimization (caching, load balancers, scaling, CDNs, etc.), so I probably won&#8217;t add to that pile here &#8212; everyone can dig into those on their own and compare them against this formula.</p><h2><strong>Real-world application</strong></h2><p>Staying on the topic of latency, I actually shared a post before about <a href="https://open.substack.com/pub/quangchientran/p/5-building-a-monitoring-platform?r=5zk2y9&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">building a monitoring system</a>, and there were 2 things I mentioned.</p><h3>1. Why p99 Matters More Than Average</h3><p>First, I emphasized the importance of percentile metrics (p50, p95, p99), especially this guy p99. And why is average latency useless here? Because in reality, it literally does nothing. It does NOT represent the actual latency that users experience.</p><p>If you tell your boss, <em>&#8220;Our average latency is 200ms,&#8221;</em> you aren&#8217;t telling the truth. You&#8217;re telling a <strong>statistic</strong>.</p><p>The &#8220;Average&#8221; is a mathematical trap. It assumes every user has a similar experience. But in a distributed system, a single request doesn&#8217;t just &#8220;happen&#8221;&#8212;it hits a load balancer, a thread pool, a database, and maybe three external APIs.</p><p>Very roughly:</p><ul><li><p><strong>p50</strong>: This is the experience of your "typical" user. Half are faster, half are slower.</p></li><li><p><strong>p95</strong>: 1 out of every 20 requests is slower than this.</p></li><li><p><strong>p99</strong>: The "1% experience." 1 out of every 100 requests hits this delay.</p></li></ul><p><strong>The Danger of Scale:</strong> If your landing page makes <strong>50 different network calls</strong> to load (images, CSS, tracking, API), the chance that a user hits a <strong>p99 delay</strong> at least once is nearly <strong>40%</strong>.</p><p>Suddenly, the &#8220;1% problem&#8221; becomes a &#8220;40% of my users are annoyed&#8221; problem. This is why we optimize for the outliers, not the average.</p><h3>2. A Debug Story From Production</h3><p>Second is a story where I debugged an issue:</p><ul><li><p>Hundreds of third-party payment webhook requests all hit the system at exactly 12 PM, causing database congestion.</p></li><li><p>At peak, the database wanted 20 vCPUs&#8230; while the system only had 2 &#128514;</p></li><li><p>Meanwhile, the server looked perfectly fine &#8212; RAM good, CPU good.</p></li></ul><p>But every day at that time, the whole system became slow like a turtle for ~30 minutes. Everything slowed down. Boss complained. Teammates complained. Customers complained. Reputation and service quality took a hit.</p><p>Now I&#8217;ll analyze it again, but this time using the latency formula above.</p><p>At that time, I don&#8217;t know if I was just too inexperienced, or I read too many tips &amp; tricks and started overthinking. In my head, I thought:</p><blockquote><p><em><strong>&#8220;If too many requests come in and we can&#8217;t handle them, just throw them into a queue and process gradually. Easy. (damn I&#8217;m a genius &#128514;)&#8221;</strong></em></p></blockquote><p>So I jumped in and designed a beautiful architecture with SQS + Lambda + reserved concurrency (this thing ensures a certain number of Lambdas are always available, and also limits how many run in parallel).</p><p>Now all webhook payment requests would be processed gradually. Let&#8217;s see how the database dares to max out CPU again &#128527;</p><p>Well&#8230; life is not a dream. Me and my teammate spent 2 weeks implementing this solution. Result?</p><ul><li><p>Nothing improved.</p></li><li><p>System still slow.</p></li><li><p>People still unhappy.</p></li><li><p>And we wasted time.</p></li></ul><p>If I had known the formula earlier, things would&#8217;ve been much simpler instead of chasing fancy stuff.</p><h4><strong>Applying the Formula: Propagation, Queueing, Service</strong></h4><p><strong>Propagation</strong></p><p>This one is hard to optimize. Third-party systems (like payment providers) connect to us &#8212; hard to control. In my case, maybe just vertically scale the database to 20 vCPUs and call it a day =]]]</p><p><strong>Queueing</strong></p><p>This is where requests wait before being processed.</p><ul><li><p>Network/router queues, CPU queues &#8594; too advanced for me =]]].</p></li><li><p>But thread pool queue &amp; connection pool &#8594; these I can control.</p></li></ul><p>So I tuned the default configs in Spring Boot to better fit my system. From now on:</p><ul><li><p>Requests are processed more sequentially.</p></li><li><p>Less fighting, less contention.</p></li><li><p>No more trying to do too many things in parallel while resources are limited.</p></li></ul><p><strong>Service</strong></p><p>Honestly, I don&#8217;t know why I didn&#8217;t think about this earlier, and kept chasing fancy architectures. The webhook processing method had MANY issues:</p><ul><li><p>Bad async chain design (if it&#8217;s already a chain, why make it async??)</p></li><li><p>Same request fetching transaction, invoice, payment again and again</p><ul><li><p>&#8594; direct pressure on database</p></li><li><p>&#8594; why not cache it?</p></li><li><p>&#8594; in-memory cache worked perfectly here</p></li></ul></li><li><p>Non-critical tasks (audit, tracking) executed directly</p><ul><li><p>&#8594; more DB overload</p></li><li><p>&#8594; I moved them to the end of the webhook</p></li><li><p>&#8594; maybe later I&#8217;ll push them into a queue, we&#8217;ll see &#128516;</p></li></ul></li><li><p>Database had too many missing important indexes AND too many useless ones</p><ul><li><p>I just tracked <code>trace_id</code> in monitoring &#8594; immediately saw which requests were slow</p></li><li><p>&#8594; reran SQL &#8594; found full table scans</p></li><li><p>As for unused indexes, every database has tools for that &#8212; just Google it (right now I forgot already&#8230; classic &#8220;learned via tips &amp; tricks&#8221; &#128514;)</p></li></ul></li></ul><p>And I didn&#8217;t even touch any &#8220;bit-level optimization&#8221; yet.</p><p>&#8594; At this point, the system was already good enough. (Know your limit, be happy with what you have &#8212; going deeper is just complex and time-consuming.)</p><p>After applying all these:</p><ul><li><p>No surprise &#8212; the things that should&#8217;ve been done from the beginning worked best.</p></li><li><p>Now the system only needs <strong>~0.5 vCPU</strong> (max was 2 before).</p></li><li><p>Maybe now I should increase concurrency for thread pool and connection pool &#128514;</p></li></ul><h2><strong>Conclusion</strong></h2><p>Understanding latency instead of memorizing tricks helped me:</p><ul><li><p>Think more clearly</p></li><li><p>Debug more effectively</p></li><li><p>Avoid unnecessary complexity</p></li></ul><p>When things are clear (like a formula), remembering tips is no longer the problem.</p><p>The real question becomes:</p><blockquote><p><strong>Does this tip actually solve my problem&#8230; or just make it more complex?</strong></p></blockquote><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[I Spent 6 Months Overthinking Environment Variables (The Solution Was Simple)]]></title><description><![CDATA[Environment variables are something every application needs today.]]></description><link>https://quangchientran.substack.com/p/environment-variables-my-6-month</link><guid isPermaLink="false">https://quangchientran.substack.com/p/environment-variables-my-6-month</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Mon, 09 Mar 2026 22:12:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Ecao!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Environment variables are something every application needs today. Big or small, sooner or later everyone has to deal with them.</p><p>People often say: <em>&#8220;This is easy. Just store them in a secure place.&#8221;</em><br>And yes, everyone knows the best practices by heart.</p><p>But for almost <strong>six months</strong>, I was completely lost trying to find the right solution.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ecao!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ecao!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Ecao!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Ecao!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Ecao!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ecao!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg" width="736" height="736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:736,&quot;width&quot;:736,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:61845,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/190362238?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ecao!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Ecao!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Ecao!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Ecao!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc5bd867-b620-4f27-9d1f-6c56eae16eff_736x736.jpeg 1456w" sizes="100vw" fetchpriority="high"></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><div><hr></div><h2>My Problem</h2><p>From my previous article, you may know that I deploy microservices using <strong>AWS ECS</strong>.</p><p>Each service has a <strong>task definition</strong>, where environment variables are mapped to <strong>Parameter Store</strong> and <strong>Secrets Manager</strong>, two AWS services designed to store configuration and secrets securely.</p><p>For <strong>storing</strong> environment variables, everything works quite well.</p><p>But for <strong>managing them in a clear and simple way</strong>, so that team members can easily <strong>add, remove, or modify variables</strong>, things become much harder.</p><p>To do that properly, you need some knowledge about:</p><ul><li><p>cloud infrastructure</p></li><li><p>IAM permissions</p></li><li><p>rollback responsibilities when something goes wrong</p></li></ul><p>So I had an idea.</p><p>I created a <strong>separate project dedicated to environment variables for all services</strong>, including both normal variables and secrets. The idea was simple: if someone wants to change something, they only need to update it there.</p><p>I also set up <strong>Infrastructure as Code with Terraform</strong> for the project. Deploying variables to Parameter Store and Secrets Manager became very easy.</p><p>But then another problem appeared.</p><div><hr></div><h2>The Headache</h2><p>My team uses <strong>GitHub</strong> (not self-hosted). All repositories are private.</p><p>But as everyone knows:</p><p><strong>Pushing environment variables directly to GitHub is almost like committing suicide.</strong></p><p>Every second, there are automated tools scanning GitHub repositories for exposed secrets&#8212;even private ones.</p><p>So the project stayed on my local machine.</p><p>But that obviously wasn&#8217;t a real solution.<br>What happens if someone else wants to add or modify variables? Should they just copy the project from my computer?</p><p>At a company where I previously worked, things were much simpler.</p><p>They hosted their own <strong>GitLab registry</strong>, everything was private, and they simply pushed the environment variable project there and connected it to a pipeline.</p><p>Very straightforward.</p><div><hr></div><h2>The Solution (Not Very Surprising)</h2><p>You might not believe it, but I spent <strong>almost half a year</strong> thinking about this problem.</p><p>I considered many ideas.</p><p>For example:</p><ul><li><p>encrypting environment variables before pushing them to GitHub</p></li><li><p>decrypting them during deployment</p></li></ul><p>But honestly, those solutions were too complex and inefficient.</p><p>My goal is always the same:<br><strong>solutions should be as simple as possible.</strong></p><p>Developers already suffer enough. There&#8217;s no need to make a simple problem complicated&#8212;because once the system becomes complex, maintenance becomes harder and mistakes become more likely.</p><p>And usually, no one understands that logic except the person who designed it.</p><p>The solution I finally used was actually something I had thought about before, but the conditions at the time didn&#8217;t allow it.</p><p><strong>AWS CodeCommit.</strong></p><p>In many ways, CodeCommit works just like GitHub. All standard Git operations work the same way.</p><p>However, when I first considered using it, AWS had temporarily <strong>disabled the creation of new repositories</strong> for CodeCommit (around 2024, if I remember correctly). So that idea had to be abandoned.</p><p>Time passed, and I still hadn&#8217;t found a good solution.</p><p>Then one day, while scrolling Facebook during <strong>AWS re:Invent 2025</strong>, I saw the news that AWS had reopened the ability to create new repositories in CodeCommit.</p><p>So I started implementing the solution immediately.<br><em>(What if they disabled it again? Haha.)</em></p><div><hr></div><h2>The Implementation</h2><p>CodeCommit repositories are protected inside the AWS account, with multiple layers of security.</p><p>I also set up a <strong>CI/CD pipeline</strong> for this project using:</p><ul><li><p><strong>AWS CodeBuild</strong></p></li><li><p><strong>AWS CodePipeline</strong></p></li></ul><p>And sorry AWS&#8230; but honestly, I&#8217;m not a big fan of these CI/CD services.</p><p>They feel <strong>too complicated</strong> for solving problems that other tools handle much more simply.</p><p>But that&#8217;s okay.</p><p><strong>If it works, it works.</strong> &#128516;</p><div><hr></div><h2>Conclusion</h2><p>Everything now runs the way I wanted.</p><p>But finding this solution definitely took <strong>too much time and sweat</strong>.</p><p>And honestly, the architecture still isn&#8217;t perfect because the source code is now split across different places (<strong>GitHub and CodeCommit</strong>).</p><p>Hopefully there will be better solutions for managing environment variables in the future.</p><p>If you have a better approach, please share it with me. I&#8217;d love to learn. &#128578;</p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[10 Years Using SQL… and I Finally Learned How Databases Actually Work]]></title><description><![CDATA[I&#8217;m a developer with almost 10 years of experience, and I&#8217;ve spent those same years working with databases.]]></description><link>https://quangchientran.substack.com/p/10-years-as-a-developer-but-only</link><guid isPermaLink="false">https://quangchientran.substack.com/p/10-years-as-a-developer-but-only</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Sun, 08 Mar 2026 23:52:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DenP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m a developer with almost 10 years of experience, and I&#8217;ve spent those same years working with databases. Most of my work has been with relational databases (RDBMS) &#8212; from PostgreSQL, Oracle, and SQL Server to MySQL. That said, for most of that time, my relationship with databases was still just a &#8220;casual acquaintance&#8221;: a few basic <code>SELECT</code>s, then <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>, without really understanding the deeper nature of the system or the components underneath it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DenP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DenP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!DenP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!DenP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!DenP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DenP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2918451,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/190336149?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.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_!DenP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!DenP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!DenP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!DenP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6f84c5-c46e-46fe-9cec-b8515f3ba38c_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></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><h2><strong>Familiar advice</strong></h2><p>If you search online for ways to optimize an SQL query, it&#8217;s not hard to find the classic &#8220;<strong>golden rules</strong>&#8221; like these:</p><blockquote><p><em>Don&#8217;t </em><code>SELECT *</code><em>; select fewer columns and the query will be much faster.</em></p></blockquote><blockquote><p><em>If a table has only a few rows, the query will be fast.</em></p></blockquote><blockquote><p><em>If a query has </em><code>ORDER BY</code><em> on a column, add an index on that column.</em></p></blockquote><blockquote><p><em>If a query is slow, think immediately about partitioning or sharding the database.</em></p></blockquote><blockquote><p><em>Changing the order of clauses can improve SQL performance.</em></p></blockquote><blockquote><p><em>A fragmented table is always bad for performance, so you need to defragment it.</em></p></blockquote><p>To be honest, I&#8217;m not here to judge whether those tips are right or wrong. But they&#8217;re only the &#8220;surface.&#8221; I usually applied them like a machine: do it, forget it, and a few months later I&#8217;d have to look them up again.</p><p>You&#8217;re not wrong &#8212; that was how I worked for many years, and I&#8217;m sure a lot of people can relate. It was basically a way of working based on &#8220;tips &amp; tricks&#8221; rather than systems thinking.</p><h2><strong>Then one day</strong></h2><p>Then one day, I watched some YouTube videos about database optimization by Tr&#7847;n Qu&#7889;c Huy, and the sentence that impressed me the most was this:</p><blockquote><p><strong>In databases, once you understand the execution plan, most of those tips and tricks on the internet become nonsense.</strong></p></blockquote><p>That was when I finally understood why, before that, I always felt a certain fear when working with databases, during interviews, or while handling database-related incidents. It came from the fact that I knew the <strong>syntax</strong>, but not the <strong>mechanism</strong>. I always felt like I had a blind spot &#8212; like, please don&#8217;t bring it up, I only know a few <code>SELECT</code><strong>s</strong>.</p><p>When I run a query, what happens underneath? What takes time? Why does it take time? Why does Oracle cost so much, while open-source databases like PostgreSQL and MySQL are already so good &#8212; why bother paying for proprietary software? Why does PostgreSQL get fragmented, and should we use <code>VACUUM</code>? Why, when I <code>DELETE</code> a record, doesn&#8217;t the table size shrink? Once you understand what is really happening underneath, all of those questions become much clearer.</p><h2><strong>A little fundamentals</strong></h2><p>This is something you can easily search online, but I&#8217;ll repeat it here because it&#8217;s very important. In many database systems, the smallest physical unit of storage and processing is <strong>not an individual row</strong>, but a <strong>page</strong> or <strong>block</strong>. Each page/block is a fixed-size or near-fixed-size chunk of data, usually a few KB to a few tens of KB depending on the system. You can think of it like a sheet of paper, and each row is written on it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qnol!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qnol!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 424w, https://substackcdn.com/image/fetch/$s_!qnol!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 848w, https://substackcdn.com/image/fetch/$s_!qnol!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 1272w, https://substackcdn.com/image/fetch/$s_!qnol!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qnol!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png" width="1380" height="752" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:752,&quot;width&quot;:1380,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1614069,&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://quangchientran.substack.com/i/190336149?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.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_!qnol!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 424w, https://substackcdn.com/image/fetch/$s_!qnol!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 848w, https://substackcdn.com/image/fetch/$s_!qnol!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.png 1272w, https://substackcdn.com/image/fetch/$s_!qnol!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febbb6cda-365b-4008-a346-dc552deac4d6_1380x752.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 the database needs to read a row, it usually works with the entire page/block that contains that row, rather than reading just one line directly from disk. So query performance depends <strong>not only on the number of rows</strong>, but also on the <strong>number of pages/blocks that must be scanned</strong>, how the data is organized, and whether the data can be served from cache or must be loaded from disk into RAM first.</p><p>A query can be fast or slow largely because of its <strong>execution plan</strong> &#8212; that is, the way the database chooses to execute the statement. Different database systems use different algorithms to generate execution plans. The same SQL can be slow if the system chooses a full table scan, but much faster if it uses an index properly.</p><p>An <strong>index</strong> is usually a separate data structure, often a B-tree, that helps speed up access by storing sorted lookup values together with a <strong>pointer to the location of the page/block containing the data</strong>. In simple terms, an index is like the table of contents in a book: it doesn&#8217;t contain the full content, but it helps you find the right page/block much faster.</p><p>An index is not perfect, and it has a downside: the more indexes you add, the slower write operations (<code>INSERT</code>, <code>DELETE</code>, <code>UPDATE</code>) become, because the database has to update the corresponding index trees as well.</p><p>In PostgreSQL, if you run the following command, it will show the physical location of the row version within its table:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;65f03be2-4794-4b4a-8f27-8bcd2d684280&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT ctid, * FROM my_table;</code></pre></div><p>In practice, databases often have caching mechanisms to a<strong>void re-analyzing everything from scratch</strong> if a query or execution plan has already been used before. However, an old plan is not always the best one for every run, because the data and query conditions can change.</p><p>A query execution flow can be summarized like this:</p><ol><li><p>Check the SQL syntax.</p></li><li><p>Check whether the table names, column names, and constraints are valid.</p></li><li><p><strong>Check whether the execution plan already exists in cache.</strong></p><ul><li><p><strong>If yes, reuse it and move to step 4.</strong></p></li><li><p><strong>If not, analyze all possible execution plans.</strong></p></li><li><p><strong>Build the detailed execution steps.</strong></p></li></ul></li><li><p><strong>Execute the SQL statement.</strong></p></li><li><p>Return the result.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ifIf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ifIf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ifIf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ifIf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ifIf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ifIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2359084,&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://quangchientran.substack.com/i/190336149?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.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_!ifIf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ifIf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ifIf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ifIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ad6c1a9-c46e-46be-bf9a-b456ecfaf47b_1536x1024.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>Among these steps, steps 3 and 4 are often the most time-consuming. In PostgreSQL, you can use the following command to inspect the execution plan, including the detailed steps and cost:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;fa238445-8909-47c4-84c0-2fe5b8366e45&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">EXPLAIN (ANALYZE, BUFFERS) + Query</code></pre></div><p>This command will show you:</p><ul><li><p><strong>Cost</strong>: the planner&#8217;s estimated cost.</p></li><li><p><strong>Actual time</strong>: the real execution time.</p></li><li><p><strong>Buffers / Shared hit / Shared read</strong>: how many pages were served from cache and how many had to be read from disk.</p></li></ul><p>If you want to optimize a query, you should:</p><ul><li><p>look at the execution plan,</p></li><li><p>check how many pages/blocks are being read,</p></li><li><p>see whether the indexes are reasonable,</p></li><li><p>and run the query for real instead of guessing whether it is slow or fast.</p></li></ul><h2><strong>A bit deeper into bloat in PostgreSQL</strong></h2><h4>MVCC mechanism</h4><p>PostgreSQL uses <strong>MVCC (Multi-Version Concurrency Control)</strong> when handling <code>UPDATE</code> or <code>DELETE</code>, allowing multiple transactions to proceed concurrently without forcing readers and writers to wait for each other in most cases. Instead of modifying a row in place, when an <code>UPDATE</code> happens, PostgreSQL usually <strong>creates a new version of the row</strong> and <strong>keeps the old version for transactions that can still see it</strong>.</p><p>Thanks to that, each transaction sees a consistent snapshot of the data at the right point in time, so &#8220;<strong>reads are usually not blocked by writes</strong>&#8221;, and &#8220;<strong>writes are usually not blocked by reads</strong>&#8221;. This is one of the reasons PostgreSQL handles <strong>concurrent workloads</strong> quite well.</p><p>That said, saying &#8220;<strong>the reader never blocks the writer</strong>&#8221; and &#8220;<strong>the writer never blocks the reader</strong>&#8221; is too absolute. In reality, PostgreSQL still uses locking in some cases, such as conflicting updates on the same row, schema locks during operations like <code>VACUUM FULL</code>, or other special operations.</p><p>When you update a row, for example <code>/users/123</code>:</p><ul><li><p>the old row <code>123</code> is copied and a new row version is created in another page at another location with the updated data,</p></li><li><p>the old row is marked as &#8220;<strong>expired</strong>&#8221;, and <em><strong>new transactions will no longer see it after the update is committed, but the database does not delete it immediately,</strong></em></p></li><li><p>as a result, on disk there can still be two row versions for the same ID <code>123</code>; one of them is simply marked as expired.</p></li></ul><p>When you run <code>SELECT</code>, the execution plan still has to deal with those pages containing dead rows, which can make things slower than necessary.</p><h4>Why deleting doesn&#8217;t free disk space immediately</h4><p>When <code>DELETE</code> happens, it works in a similar way: the row is not physically removed right away, but marked as <strong>dead</strong>. It&#8217;s like a house being labeled with a warning sign saying &#8220;<strong>do not live here, risk of collapse</strong>&#8221; while the house still remains in place and the land is not cleared for a new house.</p><p>This makes the table grow larger over time. The table may be 10 GB in size, while the live data you actually work with is only 2 GB, and the remaining 8 GB are rows marked as expired or dead, not doing anything useful.</p><h4>What VACUUM is in PostgreSQL</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JnyF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JnyF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 424w, https://substackcdn.com/image/fetch/$s_!JnyF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 848w, https://substackcdn.com/image/fetch/$s_!JnyF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 1272w, https://substackcdn.com/image/fetch/$s_!JnyF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JnyF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png" width="1456" height="955" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:955,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:952825,&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://quangchientran.substack.com/i/190336149?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.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_!JnyF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 424w, https://substackcdn.com/image/fetch/$s_!JnyF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 848w, https://substackcdn.com/image/fetch/$s_!JnyF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.png 1272w, https://substackcdn.com/image/fetch/$s_!JnyF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a1d7c3a-d1ad-49d7-a857-f8987f34177d_1482x972.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><code>VACUUM</code> is a command used to deal with table bloat in PostgreSQL. There are two approaches to it:</p><ul><li><p><strong>Standard VACUUM</strong>: PostgreSQL usually handles this automatically through autovacuum, but you can also adjust settings or run it manually if needed. <strong>Its job is to scan dead tuples and mark those positions as available for future writes</strong>. It also cleans dead tuple references in the index and updates related maps. The next time you <code>INSERT</code>, PostgreSQL can place data there instead of using new locations or adding new pages that would make the table bloat further. Note that <strong>standard </strong><code>VACUUM</code><strong> does not return disk space to the operating system</strong>; it only marks space for future PostgreSQL use.</p></li><li><p><strong>VACUUM FULL</strong>: With this command, PostgreSQL creates a new, smaller, more compact version of the table. It can <strong>return disk space to the operating system</strong>, but it also takes an exclusive lock on the table, so the table cannot be read or written while <code>VACUUM FULL</code> is running.</p></li></ul><h4>When should you use VACUUM FULL?</h4><p>Because it locks the table, <code>VACUUM FULL</code> should only be used in a few critical situations, such as:</p><ul><li><p>when you have deleted around 60&#8211;90% of a table and want to reclaim disk space and shrink the table size,</p></li><li><p>when a table has become excessively bloated, for example 100 GB in size while the actual data is only 10 GB,</p></li><li><p>when the table is mostly read-only or used for reporting and no longer changes much, so removing bloat can improve query speed.</p></li></ul><p>You should not use it on tables that are heavily read, written, or updated, because it will block the table for some amount of time, which may freeze the application and hurt performance.</p><h4>What happens if 100 transactions try to update the same row at the same time?</h4><p>Of course, there is no way for 100 transactions to all &#8220;<strong>successfully</strong>&#8221; update the same row by freely stacking on top of each other. What usually happens is a <strong>write-write conflict</strong>: the transaction that updates <strong>first creates a new version of the row(tuple),</strong> and <strong>the other transactions have to wait</strong>. When it&#8217;s their turn, they either read the new row again to <strong>re-check</strong> the condition, or they get aborted/fail depending on the isolation level and how the database is implemented.</p><p>Let&#8217;s say 100 transactions all want to &#8220;<strong>update</strong>&#8221; one row:</p><ul><li><p>The first transaction that gets the update right will create a <strong>new version of the row (tuple)</strong>.</p></li><li><p>The other transactions do <strong>not &#8220;overwrite directly&#8221;</strong> on the same version. They will be <strong>serialized in execution order</strong>, or they have to wait until the current version is done before they can continue.</p></li><li><p>If two transactions update the exact same row at the same time, the later transaction often has to <strong>re-read the new row and run the update condition again</strong>. If it no longer matches, it may no longer update anything, or it may fail with a serialization error depending on the isolation level.</p></li></ul><p><strong>Which version is kept?</strong></p><ul><li><p>The newest committed version is the version that is valid for new transactions.</p></li><li><p>Older versions only remain to serve running transactions or old snapshots.</p></li><li><p>After that, they will be cleaned up by garbage collection / vacuum / purge depending on the system.</p></li></ul><h2><strong>What about the mechanism in other types of databases?</strong></h2><p>In general, many databases that do not use MVCC like PostgreSQL will usually handle <code>UPDATE</code> and <code>DELETE</code> by <strong>modifying data in place</strong> or by <strong>locking rows/pages/tables</strong> while the change is happening.</p><h3>Common mechanism</h3><h4>In-place update</h4><p>Simpler systems will let <code>UPDATE</code> overwrite the existing row directly, instead of creating a new version like MVCC. This saves space, but it usually needs locks to prevent multiple transactions from modifying the same data at the same time and corrupting it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vqeK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vqeK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!vqeK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!vqeK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!vqeK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vqeK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2737375,&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://quangchientran.substack.com/i/190336149?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.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_!vqeK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!vqeK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!vqeK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!vqeK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b122dd-3113-4c1e-b8e3-c6afcc19ada3_1536x1024.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 a transaction wants to <code>UPDATE</code> a row, the database will:</p><ul><li><p><strong>take a lock on the row, page</strong>, or sometimes the whole table depending on the engine and isolation level,</p></li><li><p>make sure other transactions do not read/write that part of the data in a way that breaks consistency,</p></li><li><p>then write the new value into the existing data location, or update the corresponding internal storage structure.</p></li></ul><h4>Real delete</h4><p>With <code>DELETE</code>, the system may remove the row from the storage structure immediately, or mark it first and clean it up later depending on the engine. The common point is that this mechanism usually depends much more on locking and physical cleanup inside the storage engine.</p><h4>Locking-centric concurrency</h4><p>In locking-oriented systems, readers or writers may have to wait for each other more often, depending on the isolation level and the lock type. This makes the model easier to understand, but at the same time it makes concurrency less &#8220;smooth&#8221; when the workload has many mixed reads and writes.</p><h3>Advantages</h3><ul><li><p>Easier to understand and implement than MVCC in many cases.</p></li><li><p>Less need to keep multiple versions of the same row, so storage overhead is lower.</p></li><li><p><code>UPDATE</code> and <code>DELETE</code> can be physically simpler if the system supports direct overwrite well.</p></li></ul><h3>Disadvantages</h3><ul><li><p>Concurrency is usually worse than MVCC, because readers and writers block each other more easily.</p></li><li><p><strong>Bottlenecks</strong> appear more easily when many transactions touch the same area of data.</p></li><li><p>If the system has to lock too much, latency can increase under high load.</p></li></ul><h3>What happens if a transaction locks a row/page and forgets to commit or rollback?</h3><p>Then that lock is usually held until the session ends or the connection is closed/killed. During that time, other transactions that want to touch the same resource may have to wait, and if they wait too long, the application looks like it is &#8220;frozen.&#8221;</p><p>The consequences are:</p><ul><li><p>The transaction that has not ended will keep control over the resource it is modifying, so other transactions may get <strong>blocked</strong>.</p></li><li><p>It can cause deadlock or timeout: if many transactions are waiting on each other, the database system may have to choose one transaction to kill.</p></li><li><p>It can pollute the application state: the app thinks it is a &#8220;<strong>data error</strong>&#8221; but in reality <strong>the lock has not been released</strong> yet.</p></li></ul><p>If a transaction is left hanging for too long, then when the connection is closed, the process is killed, or the DBMS detects that the session is dead, the transaction will be rolled back and the lock will be released.</p><h2><strong>I&#8217;m doing better now</strong></h2><p>Anything that starts from the fundamentals is always wonderful. It helps me understand what I really want, what I&#8217;m doing, and why things work the way they do. Ten years of experience does not mean you know everything. Sometimes, taking a step back to learn the most basic things again is the fastest way to move forward. Databases are fascinating &#8212; don&#8217;t let them become a blind spot in your career :D</p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Beyond the Bill: How I Matured the Cloud Infrastructure I Manage]]></title><description><![CDATA[In the previous article, I shared how I optimized the cloud bill. But in reality, there were still many things that needed improvement. Engineering is a journey, right?]]></description><link>https://quangchientran.substack.com/p/4-how-i-optimized-our-cloud-git-workflow</link><guid isPermaLink="false">https://quangchientran.substack.com/p/4-how-i-optimized-our-cloud-git-workflow</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Fri, 06 Mar 2026 19:02:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!62gP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous article, I shared how <a href="https://open.substack.com/pub/quangchientran/p/3-how-i-reduced-aws-costs-by-50?utm_campaign=post-expanded-share&amp;utm_medium=web">I optimized the cloud bill</a>. But in reality, there were still many things that needed improvement. Engineering is a journey, right?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!62gP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!62gP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!62gP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!62gP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!62gP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!62gP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1767144,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://quangchientran.substack.com/i/190135009?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.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_!62gP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!62gP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!62gP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!62gP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95a8381a-6067-4045-ad0d-bc1a10414ad0_1376x768.png 1456w" sizes="100vw" fetchpriority="high"></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><div><hr></div><h2>Pipeline to Deploy Code</h2><p>The setup I work with uses Jenkins to run pipelines. Of course, I have worked with GitLab CI/CD before, but for me, they are all just tools. Argo CD, GitHub Actions, or AWS CodePipeline &#8212; in the end, they are simply tools to build and deploy code.</p><p>At the beginning, the pipeline I managed only had a build stage for backend projects. Deployments were still done mostly by hand. So I added new steps to automatically deploy to staging and production. Now everything is fully automated after a simple <code>git push</code>. <strong>Pretty nice</strong> &#128516;. My job now is simply to change the code and push it.</p><div><hr></div><h2>Working with Git</h2><p>I have to admit something: Even though I had used Git for a long time, I never really had an <strong>effective workflow</strong>. In a team with many developers, things could easily become messy. Every time we wanted to test a new feature, we had to ask:</p><ul><li><p><em>Can we deploy now?</em></p></li><li><p><em>Will it overwrite someone else&#8217;s code?</em></p></li><li><p><em>Is another feature still being tested?</em></p></li><li><p><em>Is it my turn to test?</em></p></li></ul><p>The main problem was simple: <strong>we only had one dev environment.</strong></p><p>At that time, my understanding of an effective git workflow was still very vague.</p><p>Then one day, while walking home from work along the river&#8212;without looking at my phone, just enjoying the fresh air and thinking randomly about git workflows&#8230; suddenly an idea came to me. Haha.</p><p>Later I found a blog post online that described almost exactly the same workflow. It wasn&#8217;t a new idea &#8212; I just hadn&#8217;t been exposed to it before! In the end, everything is about what works best for your team.</p><p><strong>My principles are very simple:</strong></p><ul><li><p>The <strong>master branch</strong> is always the most stable and correct version of the system.<br>All new features and bug fixes must start from this branch.</p></li><li><p>The <strong>develop branch</strong> is used for testing.<br>Feature branches and bug-fix branches must merge into <strong>develop</strong> to be deployed to the testing environment.</p></li><li><p>After testing is successful on <strong>develop</strong>, the feature branch can then be merged into <strong>master</strong> and deployed to <strong>production</strong>.</p></li></ul><p>With this workflow, the team has been working very smoothly so far. If problems appear later&#8230; maybe I will just go walk along the river again to think about it. &#128516;</p><div><hr></div><h2>Infrastructure (Infrastructure as Code)</h2><p>The benefits of <strong>Infrastructure as Code</strong> are well known, so I probably don&#8217;t need to explain them much. You can easily find plenty of information online or just ask AI.</p><p>After finishing the deployment of the backend infrastructure on <strong>Amazon Web Services</strong>, the next thing I did was write <strong>IaC</strong> for all the projects I worked on.</p><p>I use <strong>OpenTofu</strong> (a fork of Terraform). The idea was simple: One day, if I am no longer managing this system, at least the engineers joining will have something to help them understand what was built. And if something goes wrong, they can quickly rebuild it.</p><blockquote><p><strong>Side note:</strong> Sorry AWS, but I&#8217;m not a big fan of AWS CloudFormation. Terraform code just looks much nicer to me &#128516;.</p></blockquote><div><hr></div><h2>Monitoring</h2><p>After one year of managing this infra, the thing I&#8217;m most proud of is building a monitoring platform. At my previous job, I worked with Datadog, but my understanding was basic. I mostly just used it to read logs.</p><p>When I started my current role, I was surprised to see no monitoring platform at all. If developers wanted to read logs, they had to SSH into the server. At that moment, I felt monitoring was absolutely necessary. Without it, debugging production systems feels like <strong>fighting enemies with bare hands.</strong></p><div><hr></div><h2>Backup</h2><p>Do you know what the most valuable asset of a company is? For me, it&#8217;s <strong>data</strong>. If the data disappears, the company may disappear too.</p><p>I heard a story about a company that lost all its data after hackers gained root access and deleted everything. They asked AWS for help, but it wasn&#8217;t possible. The company shut down. That made me think. </p><p>I use <strong>AWS Backup</strong> now. With this service, backups are protected and cannot easily be deleted &#8212; even with root access. At least, within my current understanding, this feels safer. </p><p>Unless the entire AWS infrastructure collapses&#8230; which hopefully is very unlikely.</p><div><hr></div><h2>Rethinking Cron Jobs</h2><p>Almost every company needs to process large datasets periodically. Originally, we used the Spring Boot <code>@Scheduled</code> annotation. It worked, but had two problems:</p><ol><li><p>Debugging was difficult.</p></li><li><p><strong>Horizontal scaling:</strong> If multiple servers run at once, the same job might execute twice!</p></li></ol><p>My solution was simple. I moved the scheduling to <strong>AWS Lambda + Amazon EventBridge</strong>. EventBridge triggers the Lambda, which then calls an HTTP API endpoint in our service. </p><p>Everything became much easier to manage.</p><div><hr></div><h2>Conclusion</h2><p>I&#8217;m always thinking about ways to improve the systems I work on. Maybe some of these solutions look simple to others. But for me, every time I find a solution, it brings a small sense of joy. And honestly, I&#8217;m always a little proud of that. &#128521;</p><p><em>(And yes, I still take walks by the river to brainstorm!)</em> &#128522;</p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[How I Reduced the Cloud Bill by 40%: 5 Essential Mindsets]]></title><description><![CDATA[How I Slashed Our AWS Bill by 40%: A Practical Guide to Infrastructure Optimization]]></description><link>https://quangchientran.substack.com/p/3-how-i-reduced-aws-costs-by-50</link><guid isPermaLink="false">https://quangchientran.substack.com/p/3-how-i-reduced-aws-costs-by-50</guid><dc:creator><![CDATA[Quang Chien TRAN]]></dc:creator><pubDate>Fri, 06 Mar 2026 16:14:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!c_co!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Actually, there have been tons of posts about how to optimize AWS costs. I&#8217;ve read them, analyzed them, and applied what makes sense for the cloud infrastructure I manage.</p><p>At some point, your cloud infra is stable, services run smoothly, no errors, the team is happy&#8230; but then at the end of the month, you look at the bill and&#8212;well&#8230; why are you paying a few thousand dollars for just a handful of services?</p><p>Whether it&#8217;s a big company or a small one, optimizing costs for any expense always needs careful consideration. Cloud cost is no exception. If you control it well, cloud is an amazing tool. If not&#8230; your wallet slowly bleeds every month and you don&#8217;t even know why.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!c_co!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c_co!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!c_co!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!c_co!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!c_co!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!c_co!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;AWS cost optimization cover art&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="AWS cost optimization cover art" title="AWS cost optimization cover art" srcset="https://substackcdn.com/image/fetch/$s_!c_co!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!c_co!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!c_co!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!c_co!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5bd6307-6a48-4ea8-8e21-cea4713beea5_1376x768.png 1456w" sizes="100vw" fetchpriority="high"></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><h2><strong>How I optimize my infrastructure</strong></h2><h2><strong>Clean up the garbage</strong></h2><p>Yep, you read that right. In almost every infrastructure, if you don&#8217;t clean regularly, there will be a bunch of unused stuff still sitting around&#8212;and you&#8217;re paying for things that bring zero value.</p><p><strong>My approach</strong>:</p><ul><li><p><strong>S3 buckets:</strong> Delete buckets or objects from non-production environments that are no longer in use.</p></li><li><p><strong>ECR (Docker images):</strong> Implement lifecycle policies to prune old versions. By keeping only the latest 10&#8211;12 images, I stopped storage costs from ballooning and cut &#8220;zombie&#8221; storage waste by over 90%.</p></li><li><p><strong>Networking &amp; Storage:</strong> Periodically audit for Elastic IPs and EBS volumes that exist but are not attached to any instance.</p></li></ul><h2><strong>If you don&#8217;t use it, turn it off</strong></h2><p>Simple logic: you work 8 hours a day. After that, you don&#8217;t use the system&#8212;but if services are still running, you&#8217;re still paying :D</p><p>For example, non-production environments can be <strong>stopped from 8 PM to 7 AM</strong>.</p><ul><li><p>ECS &#8594; set desired count = 0</p></li><li><p>RDS &#8594; stop at night, start in the morning</p></li><li><p>EC2 &#8594; stop/start or scale auto scaling down to 0</p></li></ul><p>All of this is super easy. Just combine Lambda + EventBridge to schedule it. Fully automated, no need to click manually.</p><h2><strong>Use less, pay less</strong></h2><p>This one is obvious for S3. Setting lifecycle policies can save you quite a bit.</p><ul><li><p><strong>Hot data</strong> &#8594; keep in standard storage</p></li><li><p><strong>Cold data</strong> / logs backup &#8594; move to Glacier</p></li></ul><p>You can also set lifecycle rules for ECR to auto-delete old images instead of doing it manually.</p><h2><strong>Ask: "Can this be optimized further?"</strong></h2><p>This mindset applies to almost everything I do, not just AWS.</p><p>I&#8217;m a backend dev. Sometimes I finish a task but still feel the code isn&#8217;t clean enough, naming isn&#8217;t right, or it doesn&#8217;t follow SOLID / reusable principles &#8594; I refactor.</p><p>Same with AWS cost optimization, but even more frequently.</p><p>When deploying systems (<strong>EC2, ECS, EKS, RDS</strong>), we often over-provision resources &#8220;<strong>just to be safe.</strong>&#8221; But you still pay for all of it.</p><p>Example:</p><p>With ECS Fargate, I've seen payment services set to 4 vCPU, 8GB RAM. After running, CPU and memory usage were only ~10&#8211;20%. So I cut the config in half, then monitored again. Around 50&#8211;70% utilization is a good balance.</p><p>So the question I always keep in mind is:</p><blockquote><p><strong>&#8220;Can this system (or task) be optimized further?&#8221;</strong></p></blockquote><p>And optimization isn&#8217;t just about cost&#8212;it&#8217;s also performance, scalability, and clean code.</p><h2><strong>If AWS recommends it, just follow</strong></h2><p>Honestly, AWS engineers know their stuff. They&#8217;ve laid out best practices in the Well-Architected Framework, and tools like Amazon Q can guide you. Here&#8217;s what I implemented:</p><ul><li><p><strong>Use S3 Gateway Endpoints:</strong> This is a total &#8220;cheat code.&#8221; By adding a <strong>Gateway VPC Endpoint</strong> to your routing table, traffic to S3 stays within the AWS internal network. It doesn&#8217;t go over the public internet, it&#8217;s more secure, and most importantly&#8212;<strong>S3 Gateway Endpoints are free.</strong> No data transfer markers, no hourly fees. (Just be careful not to choose the &#8220;Interface&#8221; type unless you specifically need it, as those do have a cost!)</p></li><li><p><strong>Switch ECS Fargate to ARM:</strong> I converted our Fargate tasks from x86_64 to ARM (Graviton). It&#8217;s the &#8220;good-cheap-better&#8221; standard: it usually performs better for backend workloads and is roughly <strong>20% cheaper</strong> right out of the box.</p></li><li><p><strong>Reserved Instances &amp; Savings Plans:</strong> For stable workloads like RDS or baseline EC2, this is a no-brainer. If you know you&#8217;ll be running it for a year, commit to it and take the 30&#8211;60% discount.</p></li><li><p><strong>Ditch Public IPs:</strong> Since 2024, AWS charges for every public IPv4 address (~$3.60/month per IP). By keeping resources in private subnets and using internal communication, you save money and harden your security at the same time.</p></li><li><p><strong>Free tier</strong> is great. A lot of my Lambda, SNS, SQS, and CloudWatch usage stays within the Free Tier, so I barely pay anything.</p></li><li><p>Keep a close eye on the <strong>AWS bill</strong> and set up an <strong>AWS Budget</strong> to avoid catching issues too late. Once you notice a service suddenly getting expensive, you need to understand exactly why. In general, checking <strong>Cost Explorer</strong> every 2 or 3 days is a safe habit.</p></li><li><p>If your application uses an <strong>RDS Cluster</strong> with heavy read/write activity, leading to high I/O costs (more than 25% of the total RDS bill), it may be worth considering AWS I/O Optimized storage. In that case, storage is about 30% more expensive, but I/O cost becomes 0.</p></li></ul><p>In short, there are still many ways to save AWS costs. The most important thing is to understand the service and its pricing. Before using a service, you should first understand how it&#8217;s priced and analyze it carefully, because sometimes you jump in first, and only when the bill arrives do you ask why it&#8217;s so expensive.</p><h2><strong>Conclusion</strong></h2><p>With what I&#8217;ve done above, the AWS bill has dropped by 40% compared to before. I&#8217;m happy that everything is still running well, especially for small and medium businesses, where cost should be a top priority. Running well and cheap is still better than running well and expensive, right? &#128517;</p><p><em>&#8220;The views and optimizations shared here are my own personal engineering perspectives and do not represent the specific data or policies of any employer.&#8221;</em></p><p><em>(If you enjoy these kinds of engineering stories, you can subscribe to receive the next ones.)</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://quangchientran.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://quangchientran.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item></channel></rss>