<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sri Balaji</title>
    <description>The latest articles on DEV Community by Sri Balaji (@sri2614).</description>
    <link>https://dev.to/sri2614</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F467906%2Fee67cb27-7e15-4293-b895-091047dc03fc.jpeg</url>
      <title>DEV Community: Sri Balaji</title>
      <link>https://dev.to/sri2614</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sri2614"/>
    <language>en</language>
    <item>
      <title>Core Web Vitals &amp; Frontend Performance</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:52:46 +0000</pubDate>
      <link>https://dev.to/sri2614/core-web-vitals-frontend-performance-2ca</link>
      <guid>https://dev.to/sri2614/core-web-vitals-frontend-performance-2ca</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Your laptop lies about speed. Measure what real users feel with LCP, INP, and CLS, then spend effort on the few highest-leverage fixes that actually move those numbers instead of micro-optimizing things users never notice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The site that lied to you&lt;/li&gt;
&lt;li&gt;The principle: measure what users feel&lt;/li&gt;
&lt;li&gt;The page-load timeline, mapped to the user&lt;/li&gt;
&lt;li&gt;The three vitals at a glance&lt;/li&gt;
&lt;li&gt;Two fixes that pay for themselves&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The site that lied to you
&lt;/h2&gt;

&lt;p&gt;You build a page. On your laptop it is instant, text snaps in, images pop, buttons respond the moment you click. You ship it. Then a user opens it on a three-year-old Android phone over patchy mobile data, and it is a different product entirely: a blank screen for two seconds, then a hero image that shoves the headline down just as they reach for it, then a tap that does nothing for half a second because the main thread is busy parsing your JavaScript.&lt;/p&gt;

&lt;p&gt;Nothing changed about your code between those two experiences. What changed was the &lt;strong&gt;device&lt;/strong&gt; and the &lt;strong&gt;network&lt;/strong&gt;. Your laptop has a fast CPU, a fast connection, and a warm cache. Most of your users have none of those. The gap between "works on my machine" and "works for my users" is exactly the gap &lt;strong&gt;Core Web Vitals&lt;/strong&gt; were invented to close.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Junior frontend engineers who can build a working page but have never deliberately measured its speed. If you have ever shipped something that felt fast to you and slow to everyone else, this is the article that explains why, and what to do about it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The principle: measure what users feel
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Measure what users actually feel, on the devices they actually use, not what your laptop tells you on a good day.&lt;/p&gt;

&lt;p&gt;The one rule of performance work&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Performance is not a single number like "page weight" or "load time". It is a sequence of moments the user lives through: &lt;em&gt;Can I see it yet? Is it stable while I read? Does it respond when I touch it?&lt;/em&gt; Core Web Vitals are three metrics, each one standing in for one of those felt moments. They are deliberately user-centric, they do not care how clever your build is, only what the human in front of the screen experiences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How fast the main dish actually arrives at your table&lt;/td&gt;
&lt;td&gt;LCP, when the largest, most meaningful content appears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The waiter responding quickly when you ask for the bill&lt;/td&gt;
&lt;td&gt;INP, how fast the page reacts to your interactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your table not wobbling and spilling your drink mid-meal&lt;/td&gt;
&lt;td&gt;CLS, the layout staying put instead of jumping around&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Loading a page is like being seated at a restaurant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A restaurant where the food is fast, the staff are responsive, and the table is steady feels good even if you cannot name why. Get any one of the three wrong and the whole experience sours, exactly how a page with great LCP but a janky layout still feels broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The page-load timeline, mapped to the user
&lt;/h2&gt;

&lt;p&gt;Here is one page load as the user lives it, left to right in time. Each vital attaches to a different moment of that journey, load, stability, and interactivity are not the same event and you cannot fix them with the same lever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fcmVxdWVzdCgiUmVxdWVzdDxici8-VXNlciBjbGlja3MgdGhlIGxpbmsiKQogIGNsYXNzIG5fcmVxdWVzdCBjbGllbnQ7CiAgbl9ibGFuaygiQmxhbmsgLyBzcGlubmVyPGJyLz4nSXMgYW55dGhpbmcgaGFwcGVuaW5nPyciKQogIGNsYXNzIG5fYmxhbmsgZWRnZTsKICBuX2xjcCgiTENQPGJyLz5MYXJnZXN0IGNvbnRlbnQgcGFpbnRzIikKICBjbGFzcyBuX2xjcCBjb21wdXRlOwogIG5fY2xzKCJDTFM8YnIvPkxheW91dCBzaGlmdHMgYXMgYXNzZXRzIGxvYWQiKQogIGNsYXNzIG5fY2xzIGRhdGE7CiAgbl9pbnRlcmFjdCgiRmlyc3QgdGFwPGJyLz5Vc2VyIGNsaWNrcyBhIGJ1dHRvbiIpCiAgY2xhc3Mgbl9pbnRlcmFjdCBjbGllbnQ7CiAgbl9pbnAoIklOUDxici8-UGFnZSByZXNwb25kcyB0byB0aGUgdGFwIikKICBjbGFzcyBuX2lucCBjb21wdXRlOwogIG5faGFwcHkoIlBhZ2UgZmVlbHMgZG9uZTxici8-RmFzdCDCtyBzdGFibGUgwrcgcmVzcG9uc2l2ZSIpCiAgY2xhc3Mgbl9oYXBweSBvYnNlcnZhYmlsaXR5OwogIG5fcmVxdWVzdCAtLT4gbl9ibGFuawogIG5fYmxhbmsgLS0-IG5fbGNwCiAgbl9sY3AgLS4tPnwic2hpZnRzIGh1cnQgaGVyZSJ8IG5fY2xzCiAgbl9sY3AgLS0-IG5faW50ZXJhY3QKICBuX2ludGVyYWN0IC0tPnwicmVzcG9uc2UgZGVsYXkifCBuX2lucAogIG5faW5wIC0tPiBuX2hhcHB5%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fcmVxdWVzdCgiUmVxdWVzdDxici8-VXNlciBjbGlja3MgdGhlIGxpbmsiKQogIGNsYXNzIG5fcmVxdWVzdCBjbGllbnQ7CiAgbl9ibGFuaygiQmxhbmsgLyBzcGlubmVyPGJyLz4nSXMgYW55dGhpbmcgaGFwcGVuaW5nPyciKQogIGNsYXNzIG5fYmxhbmsgZWRnZTsKICBuX2xjcCgiTENQPGJyLz5MYXJnZXN0IGNvbnRlbnQgcGFpbnRzIikKICBjbGFzcyBuX2xjcCBjb21wdXRlOwogIG5fY2xzKCJDTFM8YnIvPkxheW91dCBzaGlmdHMgYXMgYXNzZXRzIGxvYWQiKQogIGNsYXNzIG5fY2xzIGRhdGE7CiAgbl9pbnRlcmFjdCgiRmlyc3QgdGFwPGJyLz5Vc2VyIGNsaWNrcyBhIGJ1dHRvbiIpCiAgY2xhc3Mgbl9pbnRlcmFjdCBjbGllbnQ7CiAgbl9pbnAoIklOUDxici8-UGFnZSByZXNwb25kcyB0byB0aGUgdGFwIikKICBjbGFzcyBuX2lucCBjb21wdXRlOwogIG5faGFwcHkoIlBhZ2UgZmVlbHMgZG9uZTxici8-RmFzdCDCtyBzdGFibGUgwrcgcmVzcG9uc2l2ZSIpCiAgY2xhc3Mgbl9oYXBweSBvYnNlcnZhYmlsaXR5OwogIG5fcmVxdWVzdCAtLT4gbl9ibGFuawogIG5fYmxhbmsgLS0-IG5fbGNwCiAgbl9sY3AgLS4tPnwic2hpZnRzIGh1cnQgaGVyZSJ8IG5fY2xzCiAgbl9sY3AgLS0-IG5faW50ZXJhY3QKICBuX2ludGVyYWN0IC0tPnwicmVzcG9uc2UgZGVsYXkifCBuX2lucAogIG5faW5wIC0tPiBuX2hhcHB5%3FbgColor%3D0d1017%26type%3Dpng" alt="A single page load over time: each Core Web Vital maps to a distinct moment the user perceives." width="1626" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A single page load over time: each Core Web Vital maps to a distinct moment the user perceives.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notice the order. LCP happens early (loading), CLS accumulates across the whole load (stability), and INP is measured every time the user interacts (responsiveness). You optimize them in roughly that sequence, but you only know which one to attack after you measure.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt;: Run Lighthouse in Chrome DevTools, but more importantly pull &lt;strong&gt;field data&lt;/strong&gt; from the &lt;a href="https://pagespeed.web.dev" rel="noopener noreferrer"&gt;PageSpeed Insights&lt;/a&gt; report or the CrUX dashboard. Field data is real users on real devices, that is the truth. Lab data is a controlled simulation; useful for debugging, dangerous as your only source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the biggest offender&lt;/strong&gt;: Look at which of the three vitals is in the "poor" or "needs improvement" band. Do not optimize a green metric. In the Lighthouse report, expand the failing metric to see what element or script is responsible, it names the exact LCP element and the longest tasks blocking the main thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix one thing&lt;/strong&gt;: Apply the highest-leverage change for that specific vital, an oversized hero image, a render-blocking bundle, an unsized embed. Change one variable so you can attribute the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-measure&lt;/strong&gt;: Re-run the same measurement and confirm the number actually moved. Then repeat from step two on the next-worst metric. Performance work is a loop, not a one-time pass, and field data updates over 28 days, so real-world wins take time to show.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The three vitals at a glance
&lt;/h2&gt;

&lt;p&gt;Each vital has published thresholds. Google grades the &lt;strong&gt;75th percentile&lt;/strong&gt; of your users, meaning three out of four real visits must hit the "good" bar before the metric counts as passing. Optimizing for the average hides your slowest users; the 75th percentile is what forces you to care about that mid-range phone.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Good threshold&lt;/th&gt;
&lt;th&gt;Top fixes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP (Largest Contentful Paint)&lt;/td&gt;
&lt;td&gt;Loading, how long until the largest visible element (usually a hero image or headline block) is painted&lt;/td&gt;
&lt;td&gt;≤ 2.5 s&lt;/td&gt;
&lt;td&gt;Optimize/compress the LCP image, preload it, use a CDN, remove render-blocking CSS/JS, serve modern formats (WebP/AVIF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP (Interaction to Next Paint)&lt;/td&gt;
&lt;td&gt;Interactivity, how quickly the page visually responds to taps, clicks, and key presses across the whole visit&lt;/td&gt;
&lt;td&gt;≤ 200 ms&lt;/td&gt;
&lt;td&gt;Break up long tasks, reduce main-thread JS, code-split, defer non-critical scripts, debounce expensive handlers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS (Cumulative Layout Shift)&lt;/td&gt;
&lt;td&gt;Visual stability, how much content unexpectedly jumps around as the page loads&lt;/td&gt;
&lt;td&gt;≤ 0.1&lt;/td&gt;
&lt;td&gt;Set width/height on images &amp;amp; video, reserve space for ads/embeds, avoid injecting content above existing content, preload fonts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The three Core Web Vitals, their thresholds, and where to spend your effort.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bands are the same shape for all three: &lt;strong&gt;good&lt;/strong&gt; is the threshold above, &lt;strong&gt;needs improvement&lt;/strong&gt; is the grey middle, and &lt;strong&gt;poor&lt;/strong&gt; is anything past the second cutoff (LCP &amp;gt; 4 s, INP &amp;gt; 500 ms, CLS &amp;gt; 0.25). INP replaced the older FID metric in 2024, FID only measured the &lt;em&gt;first&lt;/em&gt; interaction's delay, while INP watches every interaction, which is a far harsher and more honest test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two fixes that pay for themselves
&lt;/h2&gt;

&lt;p&gt;The two highest-leverage techniques for a junior to reach for are &lt;strong&gt;lazy-loading offscreen images&lt;/strong&gt; (helps LCP and total bytes) and &lt;strong&gt;code-splitting heavy components&lt;/strong&gt; (helps INP by shrinking the JavaScript the main thread must parse upfront). Both are one-liners in modern stacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lazy-load images you cannot see yet
&lt;/h3&gt;

&lt;p&gt;An image below the fold does not need to load before the user scrolls to it. The native &lt;code&gt;loading="lazy"&lt;/code&gt; attribute defers it for free. The critical detail: &lt;strong&gt;always set &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;&lt;/strong&gt; so the browser reserves the space, this is what prevents the layout shift (CLS) when the image finally arrives. One important exception: your LCP image (the hero) should &lt;em&gt;not&lt;/em&gt; be lazy, load it eagerly, even preload it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;below-the-fold.html&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Hero image: the LCP element. Load it eagerly + hint high priority. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero.avif"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt;
  &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Product dashboard"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Offscreen images: defer until the user scrolls near them. --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- width + height reserve the box so nothing jumps (protects CLS). --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/screenshot.avif"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"450"&lt;/span&gt;
  &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Feature screenshot"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code-split so the main thread can breathe
&lt;/h3&gt;

&lt;p&gt;A 500 KB JavaScript bundle has to be downloaded, parsed, and executed before the page is interactive, and parsing JS is one of the most expensive things a phone's CPU does. If a heavy component (a chart library, a rich editor, a modal) is not needed on first paint, load it on demand with a &lt;strong&gt;dynamic import&lt;/strong&gt;. The bundler splits it into a separate chunk that only ships when it is actually used.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dashboard.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Heavy charting lib is NOT in the initial bundle.&lt;/span&gt;
&lt;span class="c1"&gt;// It downloads only when &amp;lt;Chart /&amp;gt; first renders.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./HeavyChart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showChart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowChart&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setShowChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Show analytics&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showChart&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading chart…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Chart&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Code-splitting is not just for big libraries. Route-level splitting, loading each page's code only when the user navigates to it, is often the single biggest INP and LCP win, and most frameworks (Next.js, React Router) do it for you automatically once you stop importing everything into one shared bundle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Optimizing the wrong thing.&lt;/strong&gt; Spending a day shaving 50 ms off an already-green LCP while INP sits in the red. Always fix the worst metric first, measure before you touch anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping huge JavaScript bundles.&lt;/strong&gt; Importing an entire library to use one function, or bundling every route's code into one file. This is the number-one cause of bad INP on real devices, your laptop hides it, the phone does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsized images and embeds causing CLS.&lt;/strong&gt; An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; or ad iframe with no width/height reserves zero space, so content reflows and jumps when it loads. Cheap to fix, and a top source of "why does the page keep moving" complaints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab-only testing.&lt;/strong&gt; Trusting a single Lighthouse run on your fast laptop and declaring victory. Lab data is for debugging; &lt;strong&gt;field data&lt;/strong&gt; (real users, 75th percentile) is the score that ships. Always check both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring fonts.&lt;/strong&gt; A web font that swaps in late pushes text around (CLS) or hides it entirely (delayed LCP). Preload critical fonts and use &lt;code&gt;font-display: swap&lt;/code&gt; with a matched fallback.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance is what users &lt;em&gt;feel&lt;/em&gt;, on &lt;em&gt;their&lt;/em&gt; devices, not what your laptop reports.&lt;/li&gt;
&lt;li&gt;Three vitals: &lt;strong&gt;LCP&lt;/strong&gt; (loading, ≤ 2.5 s), &lt;strong&gt;INP&lt;/strong&gt; (interactivity, ≤ 200 ms), &lt;strong&gt;CLS&lt;/strong&gt; (stability, ≤ 0.1).&lt;/li&gt;
&lt;li&gt;Always measure with &lt;strong&gt;field data&lt;/strong&gt; at the 75th percentile, not just a lab run.&lt;/li&gt;
&lt;li&gt;Loop: measure → find the worst metric → fix one thing → re-measure → repeat.&lt;/li&gt;
&lt;li&gt;Lazy-load offscreen images (but eager-load the LCP hero) and always set width/height.&lt;/li&gt;
&lt;li&gt;Code-split heavy components with dynamic imports so the main thread parses less JS.&lt;/li&gt;
&lt;li&gt;Most CLS bugs are unsized images, embeds, and late fonts, reserve the space.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Core Web Vitals make the most sense once you understand &lt;em&gt;why&lt;/em&gt; the browser behaves the way it does. If you have not yet, read &lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;How the Browser Renders a Page&lt;/a&gt;, the render path is exactly what LCP and CLS are measuring. Then &lt;a href="https://thesimplifiedtech.com/blog/frontend-build-tooling-and-bundling" rel="noopener noreferrer"&gt;Frontend Build Tooling &amp;amp; Bundling&lt;/a&gt; shows you the mechanics behind code-splitting and shrinking the bundles that wreck INP.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer career path&lt;/a&gt; to put performance in context with the rest of the role.&lt;/li&gt;
&lt;li&gt;Re-read &lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;How the Browser Renders a Page&lt;/a&gt;, render-blocking resources and layout are the root of LCP and CLS.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://thesimplifiedtech.com/blog/frontend-build-tooling-and-bundling" rel="noopener noreferrer"&gt;Frontend Build Tooling &amp;amp; Bundling&lt;/a&gt; to set up route-level code-splitting in your own project.&lt;/li&gt;
&lt;li&gt;Run your own site through PageSpeed Insights today and write down which of the three vitals is your worst, that is your first fix.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/core-web-vitals-and-frontend-performance" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>State Management in Frontend Apps</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:03:52 +0000</pubDate>
      <link>https://dev.to/sri2614/state-management-in-frontend-apps-chd</link>
      <guid>https://dev.to/sri2614/state-management-in-frontend-apps-chd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Stop reaching for a global store too early. Identify which of the &lt;strong&gt;five kinds of state&lt;/strong&gt; you have and pick the narrowest tool that works, and never manage server state as if it were client state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You wired up a global store for a toggle&lt;/li&gt;
&lt;li&gt;Pick the narrowest scope that works&lt;/li&gt;
&lt;li&gt;The five kinds of state, by scope&lt;/li&gt;
&lt;li&gt;A cheat sheet: state type to tool&lt;/li&gt;
&lt;li&gt;Local state vs a server-cache hook in code&lt;/li&gt;
&lt;li&gt;Server state is not client state&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You wired up a global store for a toggle
&lt;/h2&gt;

&lt;p&gt;A teammate opens the codebase and finds a Redux store, three slices, a middleware, and a set of action creators. They follow the wiring to see what important, app-wide thing it manages. The answer: whether a dropdown menu is open. One boolean. It lives in a global store, reachable from every component in the app, persisted across routes, traced through DevTools, to remember if a little arrow points up or down.&lt;/p&gt;

&lt;p&gt;This is the most common failure in frontend, and it is not a beginner mistake, senior teams do it constantly. State management has a reputation for being hard, so people reach for the heaviest tool they know &lt;strong&gt;first&lt;/strong&gt;, before they have a problem that needs it. The result is apps where a one-line change touches four files, and where nobody can tell which state actually matters.&lt;/p&gt;

&lt;p&gt;The skill is not learning Redux, Zustand, or Jotai. The skill is knowing &lt;strong&gt;what kind of state you have&lt;/strong&gt; and putting it in the smallest place that works. Most state does not belong in a global store. A lot of it is not even client state at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Frontend developers, comfortable with &lt;a href="https://thesimplifiedtech.com/blog/javascript-for-the-browser" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; and React components, who keep hearing "just use a state library" and want a framework for deciding &lt;strong&gt;where&lt;/strong&gt; each piece of state should live, not which library to install. No prior Redux or Zustand experience assumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pick the narrowest scope that works
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;State should live in the narrowest scope that still works, and move outward only when something outside that scope genuinely needs it.&lt;/p&gt;

&lt;p&gt;The one rule this whole article is about&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Almost every state decision is a question of &lt;strong&gt;scope&lt;/strong&gt;: who needs to read this value, and who needs to change it? If only one component cares, it is local. If two siblings care, lift it to their parent. If the whole app cares, it might be global. The mistake is starting at the widest scope "just in case", you pay the complexity cost forever for a need that usually never arrives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A reminder only you need, on a sticky note on your own desk&lt;/td&gt;
&lt;td&gt;Local UI state, useState inside one component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A note your deskmate also needs, stuck where you both sit&lt;/td&gt;
&lt;td&gt;Lifted state, held in the shared parent of two components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A policy the whole company must see, on the lobby noticeboard&lt;/td&gt;
&lt;td&gt;Global state, a store every component can read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Putting your lunch order on the company noticeboard&lt;/td&gt;
&lt;td&gt;A toggle in a global Redux store, wrong desk entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Where you write a note depends on who needs to read it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Keep the sticky note on the right desk. Most of what gets pinned to the global noticeboard is a private reminder that only one component ever reads, it just got promoted out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five kinds of state, by scope
&lt;/h2&gt;

&lt;p&gt;Before you can choose a tool, you have to name what you are holding. Almost everything falls into one of five buckets, ordered from narrowest scope to widest. The further right you go, the more tools and ceremony you take on, so the goal is always to stay as far left as the requirement allows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fbG9jYWwoIkxvY2FsIFVJPGJyLz5vbmUgY29tcG9uZW50IikKICBjbGFzcyBuX2xvY2FsIGNsaWVudDsKICBuX2xpZnRlZCgiTGlmdGVkIC8gU2hhcmVkPGJyLz5hIGZldyBjb21wb25lbnRzIikKICBjbGFzcyBuX2xpZnRlZCBjbGllbnQ7CiAgbl9nbG9iYWwoIkdsb2JhbCBBcHA8YnIvPndob2xlIGFwcCIpCiAgY2xhc3Mgbl9nbG9iYWwgY29tcHV0ZTsKICBuX3NlcnZlcigiU2VydmVyLUNhY2hlPGJyLz5yZW1vdGUgZGF0YSIpCiAgY2xhc3Mgbl9zZXJ2ZXIgZGF0YTsKICBuX3VybCgiVVJMPGJyLz50aGUgYWRkcmVzcyBiYXIiKQogIGNsYXNzIG5fdXJsIGVkZ2U7CiAgbl9jb21wKCJDb21wb25lbnQ8YnIvPnVzZVN0YXRlIikKICBjbGFzcyBuX2NvbXAgY2xpZW50OwogIG5fcGFyZW50KCJQYXJlbnQgLyBDb250ZXh0PGJyLz5wcm9wcyBvciBjb250ZXh0IikKICBjbGFzcyBuX3BhcmVudCBjbGllbnQ7CiAgbl9zdG9yZSgiU3RvcmU8YnIvPlp1c3RhbmQgLyBSZWR1eCIpCiAgY2xhc3Mgbl9zdG9yZSBjb21wdXRlOwogIG5fcXVlcnkoIlF1ZXJ5IGNhY2hlPGJyLz5UYW5TdGFjayBRdWVyeSIpCiAgY2xhc3Mgbl9xdWVyeSBkYXRhOwogIG5fcm91dGVyKCJSb3V0ZXI8YnIvPnNlYXJjaCBwYXJhbXMiKQogIGNsYXNzIG5fcm91dGVyIGVkZ2U7CiAgbl9sb2NhbCAtLT58InR3byBuZWVkIGl0Inwgbl9saWZ0ZWQKICBuX2xpZnRlZCAtLi0-fCJldmVyeW9uZSBuZWVkcyBpdCJ8IG5fZ2xvYmFsCiAgbl9nbG9iYWwgLS4tPnwiaXQgbGl2ZXMgb24gYSBzZXJ2ZXIifCBuX3NlcnZlcgogIG5fc2VydmVyIC0uLT58Iml0IGJlbG9uZ3MgaW4gdGhlIGxpbmsifCBuX3VybAogIG5fbG9jYWwgLS4tPiBuX2NvbXAKICBuX2xpZnRlZCAtLi0-IG5fcGFyZW50CiAgbl9nbG9iYWwgLS4tPiBuX3N0b3JlCiAgbl9zZXJ2ZXIgLS4tPiBuX3F1ZXJ5CiAgbl91cmwgLS4tPiBuX3JvdXRlcg%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fbG9jYWwoIkxvY2FsIFVJPGJyLz5vbmUgY29tcG9uZW50IikKICBjbGFzcyBuX2xvY2FsIGNsaWVudDsKICBuX2xpZnRlZCgiTGlmdGVkIC8gU2hhcmVkPGJyLz5hIGZldyBjb21wb25lbnRzIikKICBjbGFzcyBuX2xpZnRlZCBjbGllbnQ7CiAgbl9nbG9iYWwoIkdsb2JhbCBBcHA8YnIvPndob2xlIGFwcCIpCiAgY2xhc3Mgbl9nbG9iYWwgY29tcHV0ZTsKICBuX3NlcnZlcigiU2VydmVyLUNhY2hlPGJyLz5yZW1vdGUgZGF0YSIpCiAgY2xhc3Mgbl9zZXJ2ZXIgZGF0YTsKICBuX3VybCgiVVJMPGJyLz50aGUgYWRkcmVzcyBiYXIiKQogIGNsYXNzIG5fdXJsIGVkZ2U7CiAgbl9jb21wKCJDb21wb25lbnQ8YnIvPnVzZVN0YXRlIikKICBjbGFzcyBuX2NvbXAgY2xpZW50OwogIG5fcGFyZW50KCJQYXJlbnQgLyBDb250ZXh0PGJyLz5wcm9wcyBvciBjb250ZXh0IikKICBjbGFzcyBuX3BhcmVudCBjbGllbnQ7CiAgbl9zdG9yZSgiU3RvcmU8YnIvPlp1c3RhbmQgLyBSZWR1eCIpCiAgY2xhc3Mgbl9zdG9yZSBjb21wdXRlOwogIG5fcXVlcnkoIlF1ZXJ5IGNhY2hlPGJyLz5UYW5TdGFjayBRdWVyeSIpCiAgY2xhc3Mgbl9xdWVyeSBkYXRhOwogIG5fcm91dGVyKCJSb3V0ZXI8YnIvPnNlYXJjaCBwYXJhbXMiKQogIGNsYXNzIG5fcm91dGVyIGVkZ2U7CiAgbl9sb2NhbCAtLT58InR3byBuZWVkIGl0Inwgbl9saWZ0ZWQKICBuX2xpZnRlZCAtLi0-fCJldmVyeW9uZSBuZWVkcyBpdCJ8IG5fZ2xvYmFsCiAgbl9nbG9iYWwgLS4tPnwiaXQgbGl2ZXMgb24gYSBzZXJ2ZXIifCBuX3NlcnZlcgogIG5fc2VydmVyIC0uLT58Iml0IGJlbG9uZ3MgaW4gdGhlIGxpbmsifCBuX3VybAogIG5fbG9jYWwgLS4tPiBuX2NvbXAKICBuX2xpZnRlZCAtLi0-IG5fcGFyZW50CiAgbl9nbG9iYWwgLS4tPiBuX3N0b3JlCiAgbl9zZXJ2ZXIgLS4tPiBuX3F1ZXJ5CiAgbl91cmwgLS4tPiBuX3JvdXRlcg%3FbgColor%3D0d1017%26type%3Dpng" alt="State types by scope, start at the left, move right only when forced." width="1583" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;State types by scope, start at the left, move right only when forced.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does only one component use it?&lt;/strong&gt;: Keep it local with useState. A form input, a hover flag, an open/closed toggle, these almost never leave the component that owns them. This is where most state should stay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do a couple of nearby components need it?&lt;/strong&gt;: Lift it to their closest common parent and pass it down as props. Two siblings sharing a selected tab? The tab index lives in the parent. No library required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does genuinely everything need it, far apart in the tree?&lt;/strong&gt;: Now consider Context or a small global store, for things like the signed-in user, theme, or feature flags. The bar is high: it must be needed in many distant places and rarely change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the data actually live on a server?&lt;/strong&gt;: Stop. This is not client state. Use a server-state library (TanStack Query, SWR, RTK Query) that handles fetching, caching, and refetching. Do not copy it into a store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should the value survive a refresh or be shareable via link?&lt;/strong&gt;: Put it in the URL. A search query, active filter, page number, or selected tab often belongs in search params so a copied link reproduces the exact view.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A cheat sheet: state type to tool
&lt;/h2&gt;

&lt;p&gt;Map each kind of state to its example and the simplest tool that fits. If you can answer "what kind of state is this?" you have already answered "what should I reach for?". Notice how few rows actually call for a global store.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;The right tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local UI&lt;/td&gt;
&lt;td&gt;Input value, dropdown open, hover, an accordion's expanded panel&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useState&lt;/code&gt; / &lt;code&gt;useReducer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifted / shared&lt;/td&gt;
&lt;td&gt;Selected row two sibling panels both read&lt;/td&gt;
&lt;td&gt;State in the common parent + props&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global app&lt;/td&gt;
&lt;td&gt;Signed-in user, theme, feature flags&lt;/td&gt;
&lt;td&gt;Context, or a small store (Zustand)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-cache&lt;/td&gt;
&lt;td&gt;Product list, user profile, anything from an API&lt;/td&gt;
&lt;td&gt;TanStack Query / SWR / RTK Query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Search term, filters, page number, active tab&lt;/td&gt;
&lt;td&gt;Router search params (&lt;code&gt;useSearchParams&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Pick the row first, the tool second.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; When in doubt, default to &lt;strong&gt;local&lt;/strong&gt;. It is trivial to lift state upward later when a second component needs it. Pulling a tangle of global state back down once everything depends on it is a refactor nobody volunteers for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Local state vs a server-cache hook in code
&lt;/h2&gt;

&lt;p&gt;Here is the contrast that matters most in practice. The first block is &lt;strong&gt;local state&lt;/strong&gt;, a value owned and changed entirely on the client. The second is &lt;strong&gt;server state&lt;/strong&gt;, data that lives in a database and is only borrowed by the browser. They look superficially similar but have completely different needs, so they get completely different tools.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Counter.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Local UI state: the client owns it outright.&lt;/span&gt;
&lt;span class="c1"&gt;// No network, no cache, no "is it stale?", just a value and a setter.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Clicked &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; times
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useUser.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server-cache state: the SERVER owns the truth.&lt;/span&gt;
&lt;span class="c1"&gt;// The library handles caching, loading, errors, and refetching.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;;

async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load user&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;);
  return res.json() as Promise&amp;lt;{ id: string; name: string }&amp;gt;;
}

export function useUser(id: string) {
  // We don't store the user in useState or a global store.
  // We describe HOW to fetch it; the cache is managed for us.
  return useQuery({
    queryKey: [&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, id],
    queryFn: () =&amp;gt; fetchUser(id),
    staleTime: 60_000, // treat as fresh for 60s before refetching
  });
}

// In a component:
//   const { data, isLoading, error } = useUser(userId);
// You get loading + error + cached data for free, no useEffect soup,
// no manual setState, no duplicating the response into a store.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counter needs nothing but &lt;code&gt;useState&lt;/code&gt;. The user needs caching, a loading flag, an error path, and a strategy for staleness, all of which the query library provides. Trying to manage the user with &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; recreates a worse version of that library by hand; putting the counter in a global store adds machinery a button never asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server state is not client state
&lt;/h2&gt;

&lt;p&gt;This is the single most clarifying idea in modern frontend, so it gets its own section. &lt;strong&gt;Client state&lt;/strong&gt; is state your app invents and owns: a toggle, a form draft, the current theme. You are the source of truth. &lt;strong&gt;Server state&lt;/strong&gt; is a copy of data that lives somewhere else, a database behind an API. You do not own it; you are borrowing a snapshot.&lt;/p&gt;

&lt;p&gt;That difference changes everything. Server state can go &lt;strong&gt;stale&lt;/strong&gt; the moment you fetch it, because someone else might change the record. It needs caching so you do not refetch on every render, refetching so it stays current, and shared loading and error states. None of those concerns exist for client state. This is why purpose-built server-state libraries exist, and why shoving API responses into a general-purpose store is a mismatch: a store has no concept of staleness, background refetch, or request deduplication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The tell-tale sign:&lt;/strong&gt; If you find yourself writing &lt;code&gt;useEffect&lt;/code&gt; to fetch data and then &lt;code&gt;setState&lt;/code&gt; to store it, and then more code to track loading and errors, you are hand-rolling a server-state library. Use a real one. It will be less code and far more correct.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you split these two, your "global store" usually shrinks to almost nothing. Most of what people pile into Redux is server state that belongs in a query cache. Take that out, and what is left, the genuinely global, client-owned stuff, is often small enough for plain Context or a tiny store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A global store for everything.&lt;/strong&gt; Reaching for Redux/Zustand on day one, then routing a dropdown toggle and a form input through it. Every trivial change becomes a four-file edit. Default to local; promote outward only when a real second consumer appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicating server data in a store.&lt;/strong&gt; Fetching a list, copying it into a global store, and then fighting to keep that copy in sync with the server. Now you have two sources of truth and stale-data bugs. Let a server-state library own the cache; read it where you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prop-drilling instead of composition.&lt;/strong&gt; Threading a prop through five intermediate components that do not use it, then "fixing" it with global state. Often the real fix is &lt;strong&gt;component composition&lt;/strong&gt;, pass JSX as children so the data-owning parent renders the consumer directly. See &lt;a href="https://thesimplifiedtech.com/blog/component-architecture-and-design-systems" rel="noopener noreferrer"&gt;Component Architecture &amp;amp; Design Systems&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context for high-frequency updates.&lt;/strong&gt; Putting fast-changing values (mouse position, every keystroke) in a single Context re-renders every consumer on every change. Context is for low-frequency, widely-read values like theme or user, not a performance-sensitive firehose.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State decisions are scope decisions: who reads it, who changes it.&lt;/li&gt;
&lt;li&gt;Pick the narrowest scope that works; move outward only when forced.&lt;/li&gt;
&lt;li&gt;Five kinds: local UI, lifted/shared, global app, server-cache, URL.&lt;/li&gt;
&lt;li&gt;Default to local &lt;code&gt;useState&lt;/code&gt;; lifting later is cheap, un-globalizing is not.&lt;/li&gt;
&lt;li&gt;Server state is not client state, give it a query library, not a store.&lt;/li&gt;
&lt;li&gt;Never duplicate server data into a global store; you create a second truth.&lt;/li&gt;
&lt;li&gt;Reach for composition before prop-drilling, and before going global.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;State lives inside components, so the cleaner your component boundaries, the more obvious every state decision becomes. Strengthen the layers above and below this one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solidify the language underneath it all in &lt;a href="https://thesimplifiedtech.com/blog/javascript-for-the-browser" rel="noopener noreferrer"&gt;JavaScript for the Browser&lt;/a&gt;, closures and references are exactly what hooks rely on.&lt;/li&gt;
&lt;li&gt;Learn how composition replaces prop-drilling in &lt;a href="https://thesimplifiedtech.com/blog/component-architecture-and-design-systems" rel="noopener noreferrer"&gt;Component Architecture &amp;amp; Design Systems&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer career path&lt;/a&gt; to see where state management sits in the bigger picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next time you feel the urge to install a state library, pause and ask one question: &lt;strong&gt;what kind of state is this, and what is the narrowest scope that works?&lt;/strong&gt; Most of the time, the answer is &lt;code&gt;useState&lt;/code&gt;, and that is a feature, not a shortcoming.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/state-management-in-frontend-apps" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Rendering Strategies: CSR, SSR, SSG &amp; ISR</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:28:16 +0000</pubDate>
      <link>https://dev.to/sri2614/rendering-strategies-csr-ssr-ssg-isr-5g45</link>
      <guid>https://dev.to/sri2614/rendering-strategies-csr-ssr-ssg-isr-5g45</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Where your HTML is built, in the browser, on the server, or at build time, decides first paint, SEO, server cost, and complexity. Match CSR, SSR, SSG, or ISR to the page, and respect &lt;strong&gt;hydration&lt;/strong&gt;, the cost everyone underestimates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The beautiful app nobody could find&lt;/li&gt;
&lt;li&gt;One sentence, then a kitchen&lt;/li&gt;
&lt;li&gt;Where the HTML actually comes from&lt;/li&gt;
&lt;li&gt;The four strategies, side by side&lt;/li&gt;
&lt;li&gt;What this looks like in config&lt;/li&gt;
&lt;li&gt;Hydration: the part everyone underestimates&lt;/li&gt;
&lt;li&gt;Common mistakes that cost you users&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The beautiful app nobody could find
&lt;/h2&gt;

&lt;p&gt;You build a gorgeous single-page app. Smooth transitions, instant client-side routing, a design the team is proud of. You ship it. Then two things happen. First, you search for it on Google and the result is a blank page with a title, because the crawler saw an empty &lt;code&gt;&amp;lt;div id="root"&amp;gt;&lt;/code&gt; and a pile of JavaScript it never ran. Second, a colleague opens it on a mid-range phone over 4G and stares at a white screen for four seconds while a 600 KB bundle downloads, parses, and finally paints the UI.&lt;/p&gt;

&lt;p&gt;Nothing is broken. The code is fine. The problem is &lt;strong&gt;where&lt;/strong&gt; the HTML got built. Your app decided to build it in the browser, at the worst possible moment, on the slowest device. That decision, the rendering strategy, is one of the highest-leverage choices in frontend, and most people make it by accident.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Frontend developers who've shipped a React/Vue/Angular app and heard "SSR", "SSG", "hydration", and "ISR" thrown around without a clear map. By the end you'll know exactly where your HTML is produced for each strategy, what each one costs, and how to pick. No framework lock-in, the ideas apply everywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One sentence, then a kitchen
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A rendering strategy is just the answer to one question: where and when is the HTML for this page built, in the browser, on the server per request, or once at build time?&lt;/p&gt;

&lt;p&gt;The whole article in a sentence&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of your HTML like food at a restaurant. The same dish can reach the customer three different ways, and each has a real trade-off between freshness, speed, and cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cook-to-order: the chef makes your dish fresh when you order. Always current, but you wait, and the kitchen works on every order.&lt;/td&gt;
&lt;td&gt;SSR, the server renders HTML on each request. Fresh data, slower first byte, work per visit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-made on the shelf: cooked once this morning, grabbed instantly. Lightning fast, but it's as fresh as this morning.&lt;/td&gt;
&lt;td&gt;SSG, HTML built once at deploy time, served from a CDN. Instant, but stale until you rebuild.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meal-prep that refreshes on a timer: a batch sits ready, and a fresh batch is quietly made every hour.&lt;/td&gt;
&lt;td&gt;ISR, static pages that regenerate in the background on an interval. Fast like static, fresher than static.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cook it yourself at the table: the kitchen hands you raw ingredients and a stove.&lt;/td&gt;
&lt;td&gt;CSR, the browser receives an empty shell plus JS and assembles the page itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same meal, three kitchens, that's the entire rendering debate.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the HTML actually comes from
&lt;/h2&gt;

&lt;p&gt;Every strategy answers the same request differently. Here's the same page request flowing to four different producers, then arriving at the browser where, for the JS-driven ones, it gets &lt;strong&gt;hydrated&lt;/strong&gt; into a live app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fcmVxKCJQYWdlIHJlcXVlc3Q8YnIvPnVzZXIgaGl0cyBhIFVSTCIpCiAgY2xhc3Mgbl9yZXEgY2xpZW50OwogIG5fYnJvd3NlcigiQnJvd3NlciAoQ1NSKTxici8-YnVpbGRzIEhUTUwgZnJvbSBKUyIpCiAgY2xhc3Mgbl9icm93c2VyIGNsaWVudDsKICBuX3NlcnZlcigiU2VydmVyIChTU1IpPGJyLz5yZW5kZXJzIHBlciByZXF1ZXN0IikKICBjbGFzcyBuX3NlcnZlciBjb21wdXRlOwogIG5fYnVpbGQoIkJ1aWxkIC8gQ0ROIChTU0fCt0lTUik8YnIvPnJlbmRlcmVkIGFoZWFkIG9mIHRpbWUiKQogIGNsYXNzIG5fYnVpbGQgZWRnZTsKICBuX2h0bWwoIkhUTUwgdG8gYnJvd3Nlcjxici8-Zmlyc3QgcGFpbnQiKQogIGNsYXNzIG5faHRtbCBjbGllbnQ7CiAgbl9oeWRyYXRlKCJIeWRyYXRlPGJyLz5KUyBhdHRhY2hlcywgcGFnZSBpbnRlcmFjdGl2ZSIpCiAgY2xhc3Mgbl9oeWRyYXRlIGNvbXB1dGU7CiAgbl9yZXEgLS0-fCJzaGlwIHNoZWxsICsgSlMifCBuX2Jyb3dzZXIKICBuX3JlcSAtLT58InJlbmRlciBub3cifCBuX3NlcnZlcgogIG5fcmVxIC0tPnwic2VydmUgcHJlYnVpbHQifCBuX2J1aWxkCiAgbl9icm93c2VyIC0tPiBuX2h0bWwKICBuX3NlcnZlciAtLT4gbl9odG1sCiAgbl9idWlsZCAtLT4gbl9odG1sCiAgbl9odG1sIC0uLT58ImF0dGFjaCBoYW5kbGVycyJ8IG5faHlkcmF0ZQ%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fcmVxKCJQYWdlIHJlcXVlc3Q8YnIvPnVzZXIgaGl0cyBhIFVSTCIpCiAgY2xhc3Mgbl9yZXEgY2xpZW50OwogIG5fYnJvd3NlcigiQnJvd3NlciAoQ1NSKTxici8-YnVpbGRzIEhUTUwgZnJvbSBKUyIpCiAgY2xhc3Mgbl9icm93c2VyIGNsaWVudDsKICBuX3NlcnZlcigiU2VydmVyIChTU1IpPGJyLz5yZW5kZXJzIHBlciByZXF1ZXN0IikKICBjbGFzcyBuX3NlcnZlciBjb21wdXRlOwogIG5fYnVpbGQoIkJ1aWxkIC8gQ0ROIChTU0fCt0lTUik8YnIvPnJlbmRlcmVkIGFoZWFkIG9mIHRpbWUiKQogIGNsYXNzIG5fYnVpbGQgZWRnZTsKICBuX2h0bWwoIkhUTUwgdG8gYnJvd3Nlcjxici8-Zmlyc3QgcGFpbnQiKQogIGNsYXNzIG5faHRtbCBjbGllbnQ7CiAgbl9oeWRyYXRlKCJIeWRyYXRlPGJyLz5KUyBhdHRhY2hlcywgcGFnZSBpbnRlcmFjdGl2ZSIpCiAgY2xhc3Mgbl9oeWRyYXRlIGNvbXB1dGU7CiAgbl9yZXEgLS0-fCJzaGlwIHNoZWxsICsgSlMifCBuX2Jyb3dzZXIKICBuX3JlcSAtLT58InJlbmRlciBub3cifCBuX3NlcnZlcgogIG5fcmVxIC0tPnwic2VydmUgcHJlYnVpbHQifCBuX2J1aWxkCiAgbl9icm93c2VyIC0tPiBuX2h0bWwKICBuX3NlcnZlciAtLT4gbl9odG1sCiAgbl9idWlsZCAtLT4gbl9odG1sCiAgbl9odG1sIC0uLT58ImF0dGFjaCBoYW5kbGVycyJ8IG5faHlkcmF0ZQ%3FbgColor%3D0d1017%26type%3Dpng" alt="One request, four places the HTML can be built, then hydration wires it up in the browser." width="1074" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One request, four places the HTML can be built, then hydration wires it up in the browser.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The request arrives&lt;/strong&gt;: A user clicks a link or types a URL. The same starting point for all four strategies, what differs is who answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone produces HTML&lt;/strong&gt;: CSR ships a near-empty shell and lets the browser build the DOM from JS. SSR runs your components on the server right now. SSG/ISR hand back HTML that was rendered earlier, at build time, or refreshed in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First paint happens&lt;/strong&gt;: The moment usable HTML hits the screen. For SSR/SSG/ISR this is fast and content-rich. For CSR it waits until the JS bundle downloads and runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration makes it interactive&lt;/strong&gt;: For any JS framework, the server-rendered HTML is static until the client JS loads, walks the DOM, and attaches event handlers. Until hydration finishes, buttons look ready but don't respond.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The four strategies, side by side
&lt;/h2&gt;

&lt;p&gt;This table is the one you'll come back to. Read it as four levers, first paint, SEO, server cost, and freshness, that you can't all max at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;First paint&lt;/th&gt;
&lt;th&gt;SEO&lt;/th&gt;
&lt;th&gt;Server cost&lt;/th&gt;
&lt;th&gt;Data freshness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSR&lt;/td&gt;
&lt;td&gt;Slow (waits for JS)&lt;/td&gt;
&lt;td&gt;Weak by default&lt;/td&gt;
&lt;td&gt;Cheapest (static host)&lt;/td&gt;
&lt;td&gt;Live (fetches on load)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR&lt;/td&gt;
&lt;td&gt;Fast (HTML ready)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Highest (work per request)&lt;/td&gt;
&lt;td&gt;Live (per request)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSG&lt;/td&gt;
&lt;td&gt;Fastest (CDN)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Near zero (just files)&lt;/td&gt;
&lt;td&gt;Stale until rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISR&lt;/td&gt;
&lt;td&gt;Fastest (CDN)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Low (occasional regen)&lt;/td&gt;
&lt;td&gt;Near-fresh (timed/on-demand)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The core trade-off: nobody wins every column.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A useful read: &lt;strong&gt;SSG and ISR look identical to the visitor&lt;/strong&gt;, both serve a prebuilt file from a CDN. The difference is purely on your side: SSG only updates when you redeploy, while ISR refreshes itself on a schedule or when triggered. &lt;strong&gt;SSR is the only one paying compute on every single visit&lt;/strong&gt;, which is exactly why "just SSR everything" gets expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in config
&lt;/h2&gt;

&lt;p&gt;You rarely hand-build these. In a framework like Next.js, the strategy is often one line, whether a fetch is cached, and whether it revalidates. The mental model maps straight onto the config.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/products/[id]/page.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SSG, fetched once at build, served forever (until next deploy)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStatic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;force-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ISR, static, but regenerate in the background at most every 60s&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getIsr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// SSR, render fresh on every request, never cache&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSsr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Notice there's no CSR line here, CSR is what you get when you fetch &lt;strong&gt;inside a client component with &lt;code&gt;useEffect&lt;/code&gt;&lt;/strong&gt; instead of in a server-rendered function. The choice of &lt;em&gt;where you fetch&lt;/em&gt; quietly decides your rendering strategy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Hydration: the part everyone underestimates
&lt;/h2&gt;

&lt;p&gt;Server-rendered HTML (SSR, SSG, ISR) reaches the browser as &lt;strong&gt;dead markup&lt;/strong&gt;, it looks right, but no click does anything yet. Hydration is the process where the client framework downloads, re-runs your components, walks the existing DOM, and attaches event listeners so the page becomes interactive.&lt;/p&gt;

&lt;p&gt;This creates a sneaky gap. Your content paints fast (great!), but there's a window where the page &lt;strong&gt;looks&lt;/strong&gt; interactive and isn't. A user taps a menu, nothing happens, they tap again, and now two clicks fire once hydration lands. The bigger your JS bundle, the longer that dead window, which is why "server-rendered" alone doesn't guarantee a fast-feeling app.&lt;/p&gt;

&lt;p&gt;The modern answer is to &lt;strong&gt;hydrate less&lt;/strong&gt;. React Server Components and similar approaches split your tree: components that are purely presentational render on the server and ship as plain HTML with &lt;em&gt;zero&lt;/em&gt; client JS, while only the genuinely interactive bits (a search box, a cart button) ship and hydrate. This hybrid model keeps the fast first paint of static rendering while shrinking the hydration cost that used to come bundled with it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server Components&lt;/strong&gt;, run on the server, send HTML, ship no JS to the client. Use for layout, content, data display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Components&lt;/strong&gt;, ship JS and hydrate. Use only where you need state, effects, or event handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The win&lt;/strong&gt;, you pay hydration cost only for the interactive islands, not the whole page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost you users
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CSR for a content or marketing site.&lt;/strong&gt; Blogs, docs, landing pages, e-commerce listings, anything that lives or dies by search traffic, should never be pure client-rendered. Crawlers and social previews see an empty shell, and your first paint is gated on a bundle. Pick SSG or ISR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring hydration cost.&lt;/strong&gt; Shipping a 500 KB bundle and assuming SSR "made it fast", the HTML paints quickly but the page is dead until hydration finishes. Measure interactivity (INP / Total Blocking Time), not just first paint, and cut the bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR-ing everything.&lt;/strong&gt; Rendering every page per request when 90% of it is identical for every user burns server compute and adds latency you didn't need. If the data doesn't change per request, it should be SSG or ISR, not SSR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting freshness with SSG.&lt;/strong&gt; Pure static means stale until redeploy. If prices, inventory, or article edits need to show up within minutes, reach for ISR's &lt;code&gt;revalidate&lt;/code&gt; instead of triggering a full rebuild for every change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating it as one global setting.&lt;/strong&gt; Modern frameworks let you choose per route. Your marketing pages can be SSG, your dashboard SSR, your product pages ISR, in the same app. Don't pick one strategy for everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A rendering strategy is just &lt;strong&gt;where and when your HTML is built&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSR&lt;/strong&gt; builds in the browser, cheap to host, weak SEO, slow first paint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR&lt;/strong&gt; builds on the server per request, fresh and SEO-strong, but pays compute every visit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSG&lt;/strong&gt; builds once at deploy, fastest and cheapest, but stale until you rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISR&lt;/strong&gt; is SSG that refreshes itself on a timer, static speed, near-fresh data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration&lt;/strong&gt; is the gap between "looks ready" and "actually responds", measure it, shrink the bundle.&lt;/li&gt;
&lt;li&gt;The modern hybrid (Server Components) ships HTML for most of the page and hydrates only the interactive islands. Choose &lt;strong&gt;per route&lt;/strong&gt;, not per app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Rendering strategy decides &lt;em&gt;where&lt;/em&gt; the HTML is built, but to reason about why first paint and hydration feel the way they do, you need to understand what the browser does with that HTML once it arrives, and how to measure the result.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;How the Browser Renders a Page&lt;/a&gt;, the parse → layout → paint pipeline that turns your HTML into pixels.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/core-web-vitals-and-frontend-performance" rel="noopener noreferrer"&gt;Core Web Vitals &amp;amp; Frontend Performance&lt;/a&gt;, how to measure LCP, INP, and CLS so you can prove a strategy change actually helped.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer path&lt;/a&gt; to put rendering, performance, and architecture together.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/rendering-strategies-csr-ssr-ssg" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>JavaScript for the Browser</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:03:33 +0000</pubDate>
      <link>https://dev.to/sri2614/javascript-for-the-browser-3gj5</link>
      <guid>https://dev.to/sri2614/javascript-for-the-browser-3gj5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Internalize the single-threaded event loop and the sync, microtask, macrotask ordering it enforces. That one model explains promises, event delegation, and why a &lt;strong&gt;long task&lt;/strong&gt; freezes your UI, plus how to avoid blocking paint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The click that froze a page&lt;/li&gt;
&lt;li&gt;One chef, one thread&lt;/li&gt;
&lt;li&gt;The picture: how the event loop works&lt;/li&gt;
&lt;li&gt;Ordering: sync vs microtask vs macrotask&lt;/li&gt;
&lt;li&gt;Events, delegation, and async in real code&lt;/li&gt;
&lt;li&gt;Why long tasks block paint&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The click that froze a page
&lt;/h2&gt;

&lt;p&gt;You wire up a button. On click, it filters a list of 50,000 rows in a tight loop and re-renders. In testing it felt fine. In production a user clicks it and the &lt;strong&gt;entire page locks up&lt;/strong&gt;, the spinner stops spinning, hover states stick, other buttons do nothing, even the browser's own tab feels dead. For two whole seconds nothing responds. Then it snaps back to life as if nothing happened.&lt;/p&gt;

&lt;p&gt;Nothing crashed. No error in the console. The page wasn't "slow" in the network sense, the work was already done locally. So what happened? The answer is the single most important thing a frontend engineer can understand: &lt;strong&gt;JavaScript in the browser runs on one thread, and while your code runs, nothing else can.&lt;/strong&gt; Not animation, not clicks, not paint. Once you internalize the runtime model, async stops being magic and bugs like this become obvious.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Frontend engineers who can write JavaScript but get surprised by async ordering, frozen UIs, or memory leaks from event listeners. If you've ever wondered why a &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; doesn't run &lt;em&gt;immediately&lt;/em&gt;, or why a Promise resolves "before" a timer, this is for you. No prior knowledge of the event loop assumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One chef, one thread
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript in the browser is a single-threaded runtime: one call stack, doing one thing at a time. "Async" doesn't mean parallel, it means "do this later, when I'm free."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cleanest mental model is a kitchen with exactly &lt;strong&gt;one chef&lt;/strong&gt;. The chef can only do one task at a time. They have two notebooks. The first is a regular &lt;strong&gt;to-do list&lt;/strong&gt; (tasks): "chop onions," "plate dish 4." The second is a &lt;strong&gt;priority list&lt;/strong&gt; (microtasks): small urgent notes the chef promises to clear &lt;em&gt;before&lt;/em&gt; starting the next to-do item.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The single chef&lt;/td&gt;
&lt;td&gt;The one JavaScript thread (the call stack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The dish the chef is cooking right now&lt;/td&gt;
&lt;td&gt;The function currently on the call stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sous-chefs handling the oven &amp;amp; deliveries&lt;/td&gt;
&lt;td&gt;Web APIs (timers, fetch, DOM events), run off-thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The regular to-do list&lt;/td&gt;
&lt;td&gt;The task (macrotask) queue, clicks, timers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The urgent priority list, cleared first&lt;/td&gt;
&lt;td&gt;The microtask queue, promise callbacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The chef checking lists when hands are free&lt;/td&gt;
&lt;td&gt;The event loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The kitchen maps directly onto the JavaScript runtime.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The crucial rule: the chef finishes the current dish &lt;strong&gt;completely&lt;/strong&gt; before looking at either list. They never abandon a half-plated dish to answer the door. That "run to completion" rule is exactly why a long task freezes everything, and why ordering between timers and promises is so precise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture: how the event loop works
&lt;/h2&gt;

&lt;p&gt;Here is the whole runtime in one diagram. Your synchronous code runs on the &lt;strong&gt;call stack&lt;/strong&gt;. Anything async (a timer, a fetch, a click listener) is handed to &lt;strong&gt;Web APIs&lt;/strong&gt;, which run outside the thread and, when done, drop a callback into a queue. The &lt;strong&gt;event loop&lt;/strong&gt; is the part that, whenever the stack is empty, drains the microtask queue fully, then takes one macrotask, then repeats.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fc3RhY2soIkNhbGwgU3RhY2s8YnIvPnJ1bnMgb25lIGZyYW1lIGF0IGEgdGltZSIpCiAgY2xhc3Mgbl9zdGFjayBjb21wdXRlOwogIG5fd2ViYXBpcygiV2ViIEFQSXM8YnIvPnRpbWVycyDCtyBmZXRjaCDCtyBET00gZXZlbnRzIikKICBjbGFzcyBuX3dlYmFwaXMgZXh0ZXJuYWw7CiAgbl9taWNybygiTWljcm90YXNrIFF1ZXVlPGJyLz5wcm9taXNlIGNhbGxiYWNrcyIpCiAgY2xhc3Mgbl9taWNybyBxdWV1ZTsKICBuX21hY3JvKCJUYXNrIFF1ZXVlPGJyLz50aW1lcnMgwrcgY2xpY2tzIChtYWNyb3Rhc2tzKSIpCiAgY2xhc3Mgbl9tYWNybyBxdWV1ZTsKICBuX2xvb3AoIkV2ZW50IExvb3A8YnIvPmRyYWluIG1pY3JvLCB0aGVuIDEgbWFjcm8iKQogIGNsYXNzIG5fbG9vcCBvYnNlcnZhYmlsaXR5OwogIG5fc3RhY2sgLS0-fCJvZmZsb2FkIGFzeW5jInwgbl93ZWJhcGlzCiAgbl93ZWJhcGlzIC0uLT58InJlc29sdmUoKSJ8IG5fbWljcm8KICBuX3dlYmFwaXMgLS4tPnwidGltZXIvY2xpY2sifCBuX21hY3JvCiAgbl9taWNybyAtLT58ImFsbCBmaXJzdCJ8IG5fbG9vcAogIG5fbWFjcm8gLS4tPnwib25lIGF0IGEgdGltZSJ8IG5fbG9vcAogIG5fbG9vcCAtLT58InB1c2ggY2FsbGJhY2sifCBuX3N0YWNr%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fc3RhY2soIkNhbGwgU3RhY2s8YnIvPnJ1bnMgb25lIGZyYW1lIGF0IGEgdGltZSIpCiAgY2xhc3Mgbl9zdGFjayBjb21wdXRlOwogIG5fd2ViYXBpcygiV2ViIEFQSXM8YnIvPnRpbWVycyDCtyBmZXRjaCDCtyBET00gZXZlbnRzIikKICBjbGFzcyBuX3dlYmFwaXMgZXh0ZXJuYWw7CiAgbl9taWNybygiTWljcm90YXNrIFF1ZXVlPGJyLz5wcm9taXNlIGNhbGxiYWNrcyIpCiAgY2xhc3Mgbl9taWNybyBxdWV1ZTsKICBuX21hY3JvKCJUYXNrIFF1ZXVlPGJyLz50aW1lcnMgwrcgY2xpY2tzIChtYWNyb3Rhc2tzKSIpCiAgY2xhc3Mgbl9tYWNybyBxdWV1ZTsKICBuX2xvb3AoIkV2ZW50IExvb3A8YnIvPmRyYWluIG1pY3JvLCB0aGVuIDEgbWFjcm8iKQogIGNsYXNzIG5fbG9vcCBvYnNlcnZhYmlsaXR5OwogIG5fc3RhY2sgLS0-fCJvZmZsb2FkIGFzeW5jInwgbl93ZWJhcGlzCiAgbl93ZWJhcGlzIC0uLT58InJlc29sdmUoKSJ8IG5fbWljcm8KICBuX3dlYmFwaXMgLS4tPnwidGltZXIvY2xpY2sifCBuX21hY3JvCiAgbl9taWNybyAtLT58ImFsbCBmaXJzdCJ8IG5fbG9vcAogIG5fbWFjcm8gLS4tPnwib25lIGF0IGEgdGltZSJ8IG5fbG9vcAogIG5fbG9vcCAtLT58InB1c2ggY2FsbGJhY2sifCBuX3N0YWNr%3FbgColor%3D0d1017%26type%3Dpng" alt="The browser JavaScript runtime: stack ↔ Web APIs → queues → event loop → back to the stack." width="1251" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The browser JavaScript runtime: stack ↔ Web APIs → queues → event loop → back to the stack.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User clicks a button&lt;/strong&gt;: The browser's event system (a Web API) registers the click and queues your handler as a macrotask. Your currently-running code, if any, is not interrupted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The stack empties&lt;/strong&gt;: Whatever was running finishes and returns. The call stack is now empty, the event loop's signal that it may pick up new work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microtasks drain first&lt;/strong&gt;: Before touching the click, the loop empties the entire microtask queue, every pending promise &lt;code&gt;.then&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; continuation runs to completion (and any microtasks they schedule run too).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One macrotask runs&lt;/strong&gt;: Now the loop takes exactly one task, your click handler, and pushes it onto the stack. It runs to completion, start to finish, uninterrupted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render gets a turn&lt;/strong&gt;: Only after the handler returns and microtasks are drained can the browser repaint. Then the loop comes back for the next macrotask.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Ordering: sync vs microtask vs macrotask
&lt;/h2&gt;

&lt;p&gt;This is the table to memorize. Given this code, &lt;code&gt;console.log("A"); setTimeout(() =&amp;gt; console.log("D"), 0); Promise.resolve().then(() =&amp;gt; console.log("C")); console.log("B");&lt;/code&gt;, the output is &lt;strong&gt;A, B, C, D&lt;/strong&gt;. Synchronous lines run first, then all microtasks (the promise), then the macrotask (the timer), even though the timer was scheduled with &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synchronous&lt;/td&gt;
&lt;td&gt;top-level code, function bodies&lt;/td&gt;
&lt;td&gt;Right now, on the stack, in order&lt;/td&gt;
&lt;td&gt;A, then B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microtask&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Promise.then&lt;/code&gt;, &lt;code&gt;await&lt;/code&gt; continuation, &lt;code&gt;queueMicrotask&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;After the stack empties, ALL of them, before any macrotask&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Macrotask&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;setInterval&lt;/code&gt;, click/DOM events, &lt;code&gt;fetch&lt;/code&gt; callback dispatch&lt;/td&gt;
&lt;td&gt;One per loop turn, after microtasks drain&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Why A, B, C, D and not A, B, D, C, three priority tiers, drained in order.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;setTimeout(fn, 0) is not immediate:&lt;/strong&gt; The &lt;code&gt;0&lt;/code&gt; is a &lt;em&gt;minimum&lt;/em&gt; delay, not a guarantee. The callback is a macrotask, so it waits behind every pending microtask and behind the browser's own work. A &lt;code&gt;Promise.resolve().then(...)&lt;/code&gt; scheduled &lt;em&gt;after&lt;/em&gt; it will still run &lt;em&gt;first&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Events, delegation, and async in real code
&lt;/h2&gt;

&lt;p&gt;Two patterns cover most real frontend work: &lt;strong&gt;event delegation&lt;/strong&gt; (one listener on a parent instead of one per child, leaning on event bubbling) and &lt;strong&gt;&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;&lt;/strong&gt; for network calls. Here both are in a small, complete example, a list where clicking any item fetches its detail.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;list.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ONE listener on the container, not one per &amp;lt;li&amp;gt;.&lt;/span&gt;
&lt;span class="c1"&gt;// Clicks on children bubble up to here, that's delegation.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// event.target is the actual clicked node; find the row it lives in.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;li[data-id]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// clicked the gap between items, ignore.&lt;/span&gt;

  &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// await yields the thread back to the event loop while the&lt;/span&gt;
    &lt;span class="c1"&gt;// network request runs off-thread in a Web API.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`/api/items/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{item.dataset.id}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;);
    if (!res.ok) throw new Error(&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;HTTP &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{res.status}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;);
    const data = await res.json();

    // DOM manipulation: build a node, then attach it.
    const detail = document.createElement("p");
    detail.textContent = data.description;
    item.appendChild(detail);
  } catch (err) {
    item.textContent = "Failed to load, tap to retry.";
    console.error(err);
  } finally {
    item.classList.remove("loading");
  }
});
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why delegation wins: it works for rows added &lt;strong&gt;after&lt;/strong&gt; the listener was attached (no rebinding), it uses &lt;strong&gt;one&lt;/strong&gt; listener instead of thousands, and there's nothing to clean up per row. The &lt;code&gt;await&lt;/code&gt; lines are where the thread is &lt;em&gt;handed back&lt;/em&gt;, between them the browser is free to paint and handle other clicks. That's cooperative async on a single thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why long tasks block paint
&lt;/h2&gt;

&lt;p&gt;Now the frozen page makes sense. Rendering, style, layout, paint, is &lt;strong&gt;also&lt;/strong&gt; work the one thread must do, scheduled between macrotasks. The browser wants to repaint roughly every 16ms (60fps). But it can only do that &lt;strong&gt;when the call stack is empty&lt;/strong&gt;. While your filter-50,000-rows handler runs, the stack is never empty, so the loop never gets a turn, so no paint, no clicks, no animation. The thread is busy and there is only one.&lt;/p&gt;

&lt;p&gt;A task that runs longer than ~50ms is officially a &lt;strong&gt;"long task"&lt;/strong&gt;, long enough that users perceive jank. The fix is never "make it async" by sprinkling promises (microtasks run on the &lt;em&gt;same&lt;/em&gt; thread and still block paint). The fix is to &lt;strong&gt;break the work up&lt;/strong&gt;, &lt;code&gt;yield&lt;/code&gt; to the event loop between chunks (e.g. &lt;code&gt;await scheduler.yield()&lt;/code&gt; or chunk across &lt;code&gt;setTimeout&lt;/code&gt;/&lt;code&gt;requestIdleCallback&lt;/code&gt;), or move the heavy computation off-thread entirely with a &lt;strong&gt;Web Worker&lt;/strong&gt;, which is a genuinely separate thread.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Quick test: if &lt;code&gt;await Promise.resolve()&lt;/code&gt; in the middle of your loop does NOT unfreeze the UI, the work is CPU-bound and belongs in a Web Worker or needs to be chunked with a macrotask that lets paint in between. Microtasks alone won't save you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Blocking the main thread.&lt;/strong&gt; Heavy loops, giant JSON parses, or synchronous layout reads in a handler freeze everything. Chunk the work, defer it, or offload to a Web Worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaking listeners.&lt;/strong&gt; Every &lt;code&gt;addEventListener&lt;/code&gt; you never remove keeps its closure, and the DOM nodes it references, alive. In SPAs that re-mount components, this is the classic memory leak. Remove listeners on teardown, or use delegation so there's only one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not delegating.&lt;/strong&gt; Attaching a listener to every item in a list is slower to set up, breaks for dynamically-added items, and multiplies cleanup. One listener on the parent + &lt;code&gt;event.target.closest()&lt;/code&gt; is almost always better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;await&lt;/code&gt; inside a loop.&lt;/strong&gt; &lt;code&gt;for (const id of ids) { await fetch(id); }&lt;/code&gt; runs requests strictly one after another. If they're independent, fire them together with &lt;code&gt;await Promise.all(ids.map(fetch))&lt;/code&gt;, same thread, but the waiting overlaps instead of stacking.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser JavaScript is &lt;strong&gt;single-threaded&lt;/strong&gt;: one call stack, one thing at a time.&lt;/li&gt;
&lt;li&gt;Async work is offloaded to &lt;strong&gt;Web APIs&lt;/strong&gt;; their callbacks return via queues.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;event loop&lt;/strong&gt; drains ALL microtasks, then takes ONE macrotask, then lets the page paint.&lt;/li&gt;
&lt;li&gt;Order is &lt;strong&gt;sync → microtasks (promises) → macrotasks (setTimeout, events)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; is a macrotask, it waits behind every pending promise.&lt;/li&gt;
&lt;li&gt;A handler runs &lt;strong&gt;to completion&lt;/strong&gt;; while it runs, no paint, no clicks, that's why long tasks freeze the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegate&lt;/strong&gt; events to a parent, &lt;strong&gt;clean up&lt;/strong&gt; listeners, and &lt;strong&gt;chunk or offload&lt;/strong&gt; CPU-heavy work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Once the runtime model clicks, the rest of frontend engineering layers cleanly on top. Two natural next reads: understanding what the browser does &lt;em&gt;after&lt;/em&gt; your handler returns, and how to manage the data your handlers produce without re-introducing the chaos manual DOM updates create.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;How the Browser Renders a Page&lt;/a&gt;, what happens in that paint slot the event loop hands the browser: style, layout, and compositing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/state-management-in-frontend-apps" rel="noopener noreferrer"&gt;State Management in Frontend Apps&lt;/a&gt;, once DOM updates get complex, move state out of the DOM and let a framework re-render for you.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer path&lt;/a&gt; to see where the runtime, rendering, and frameworks fit together.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/javascript-for-the-browser" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>CSS Fundamentals: The Box Model &amp; Layout</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Fri, 09 Oct 2026 17:15:26 +0000</pubDate>
      <link>https://dev.to/sri2614/css-fundamentals-the-box-model-layout-48hf</link>
      <guid>https://dev.to/sri2614/css-fundamentals-the-box-model-layout-48hf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; CSS stops fighting you once the box model, the cascade, and normal flow click. The practical payoff: reach for &lt;strong&gt;Flexbox&lt;/strong&gt; for one dimension and Grid for two, and finally know why that div would not center.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why won't this div center?&lt;/li&gt;
&lt;li&gt;Every element is a box&lt;/li&gt;
&lt;li&gt;The box model, layer by layer&lt;/li&gt;
&lt;li&gt;Normal flow: what happens before you do anything&lt;/li&gt;
&lt;li&gt;Flexbox vs Grid: one dimension or two&lt;/li&gt;
&lt;li&gt;Build it: a responsive layout&lt;/li&gt;
&lt;li&gt;The cascade &amp;amp; specificity&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why won't this div center?
&lt;/h2&gt;

&lt;p&gt;Every developer has lived this moment. You have a box. You want it in the middle of the screen. You try &lt;code&gt;margin: auto&lt;/code&gt;. Nothing. You try &lt;code&gt;text-align: center&lt;/code&gt;. The text moves, the box doesn't. You add &lt;code&gt;padding&lt;/code&gt;, and now the box is &lt;em&gt;bigger&lt;/em&gt; instead of &lt;em&gt;centered&lt;/em&gt;. Twenty minutes later you are googling "how to center a div" for the fortieth time in your career, half-convinced CSS is broken.&lt;/p&gt;

&lt;p&gt;CSS is not broken. It is doing &lt;em&gt;exactly&lt;/em&gt; what you told it to, you just don't yet have the mental model for what "it" is. Once you internalize four ideas, the &lt;strong&gt;box model&lt;/strong&gt;, the &lt;strong&gt;cascade&lt;/strong&gt;, &lt;strong&gt;normal flow&lt;/strong&gt;, and &lt;strong&gt;modern layout&lt;/strong&gt; (Flexbox and Grid), CSS stops feeling like a slot machine and starts feeling like a language. This article installs those four models.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Anyone who can write a little HTML and CSS but feels like layout "just happens to" them rather than something they control. If you've ever pasted a &lt;code&gt;display: flex&lt;/code&gt; from Stack Overflow without knowing why it worked, this is for you. No build tools, no framework, just CSS and a browser.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Every element is a box
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;In CSS, everything is a rectangular box, and layout is just the art of sizing those boxes and arranging them relative to each other.&lt;/p&gt;

&lt;p&gt;The one sentence that unlocks CSS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, that &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, that &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, even an inline &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, the browser draws each one as a box. And each box is built in layers, like a parcel ready for shipping. Picture mailing a fragile mug:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The mug itself&lt;/td&gt;
&lt;td&gt;content, your text or image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bubble wrap snug around the mug&lt;/td&gt;
&lt;td&gt;padding, space inside the box, around the content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The cardboard box walls&lt;/td&gt;
&lt;td&gt;border, the visible edge of the box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty space between this parcel and the next on the shelf&lt;/td&gt;
&lt;td&gt;margin, space outside the box, pushing neighbours away&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Content out, padding hugs it, border wraps it, margin keeps the neighbours at arm's length.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Padding is &lt;em&gt;inside&lt;/em&gt; the box and shares its background colour. Margin is &lt;em&gt;outside&lt;/em&gt; and is always transparent. Get those two confused and you will spend hours wondering why a background colour bleeds where you didn't expect it, almost always a padding-vs-margin mix-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The box model, layer by layer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY29udGVudCgiQ29udGVudDxici8-dGV4dCAvIGltYWdlIikKICBjbGFzcyBuX2NvbnRlbnQgZGF0YTsKICBuX3BhZGRpbmcoIlBhZGRpbmc8YnIvPmluc2lkZSBzcGFjaW5nIikKICBjbGFzcyBuX3BhZGRpbmcgY29tcHV0ZTsKICBuX2JvcmRlcigiQm9yZGVyPGJyLz52aXNpYmxlIGVkZ2UiKQogIGNsYXNzIG5fYm9yZGVyIGVkZ2U7CiAgbl9tYXJnaW4oIk1hcmdpbjxici8-b3V0c2lkZSBzcGFjaW5nIikKICBjbGFzcyBuX21hcmdpbiBleHRlcm5hbDsKICBuX2NvbnRlbnQgLS0-fCJ3cmFwcGVkIGJ5Inwgbl9wYWRkaW5nCiAgbl9wYWRkaW5nIC0tPnwid3JhcHBlZCBieSJ8IG5fYm9yZGVyCiAgbl9ib3JkZXIgLS0-fCJ3cmFwcGVkIGJ5Inwgbl9tYXJnaW4%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY29udGVudCgiQ29udGVudDxici8-dGV4dCAvIGltYWdlIikKICBjbGFzcyBuX2NvbnRlbnQgZGF0YTsKICBuX3BhZGRpbmcoIlBhZGRpbmc8YnIvPmluc2lkZSBzcGFjaW5nIikKICBjbGFzcyBuX3BhZGRpbmcgY29tcHV0ZTsKICBuX2JvcmRlcigiQm9yZGVyPGJyLz52aXNpYmxlIGVkZ2UiKQogIGNsYXNzIG5fYm9yZGVyIGVkZ2U7CiAgbl9tYXJnaW4oIk1hcmdpbjxici8-b3V0c2lkZSBzcGFjaW5nIikKICBjbGFzcyBuX21hcmdpbiBleHRlcm5hbDsKICBuX2NvbnRlbnQgLS0-fCJ3cmFwcGVkIGJ5Inwgbl9wYWRkaW5nCiAgbl9wYWRkaW5nIC0tPnwid3JhcHBlZCBieSJ8IG5fYm9yZGVyCiAgbl9ib3JkZXIgLS0-fCJ3cmFwcGVkIGJ5Inwgbl9tYXJnaW4%3FbgColor%3D0d1017%26type%3Dpng" alt="The four nested layers of every CSS box, from the content out to the margin." width="910" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The four nested layers of every CSS box, from the content out to the margin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read it from the inside out. To find how much horizontal room an element &lt;em&gt;actually&lt;/em&gt; takes on screen by default, you add it all up. Here is the walkthrough for a box declared &lt;code&gt;width: 200px&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the content width&lt;/strong&gt;: You wrote &lt;code&gt;width: 200px&lt;/code&gt;. By default that 200px applies to the content box only, the mug, not the packaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add padding to both sides&lt;/strong&gt;: &lt;code&gt;padding: 20px&lt;/code&gt; adds 20px left + 20px right = 40px. The box is now 240px wide on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the border to both sides&lt;/strong&gt;: &lt;code&gt;border: 5px solid&lt;/code&gt; adds 5px left + 5px right = 10px. Now 250px.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Margin pushes neighbours, not the box&lt;/strong&gt;: &lt;code&gt;margin: 10px&lt;/code&gt; doesn't change the box's own width, it reserves 10px of empty space &lt;em&gt;outside&lt;/em&gt; it. The rendered box stays 250px; its neighbours just keep their distance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feel the surprise&lt;/strong&gt;: You asked for 200px and got a 250px box. This off-by-padding-and-border math is the #1 reason CSS layouts overflow, and &lt;code&gt;box-sizing&lt;/code&gt; (next section) makes it vanish.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Normal flow: what happens before you do anything
&lt;/h2&gt;

&lt;p&gt;Before any &lt;code&gt;flex&lt;/code&gt; or &lt;code&gt;grid&lt;/code&gt;, the browser already lays elements out using &lt;strong&gt;normal flow&lt;/strong&gt;. The two rules worth memorizing: &lt;strong&gt;block&lt;/strong&gt; elements (&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;) stack vertically and take the full available width. &lt;strong&gt;Inline&lt;/strong&gt; elements (&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;) sit side by side on a line and only take as much width as their content.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;width&lt;/code&gt; on a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; seems to do nothing (inline boxes ignore it) and why two &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s never sit beside each other on their own (block boxes claim the whole row). For years, developers fought this with &lt;code&gt;float&lt;/code&gt; to force side-by-side layout. Don't, floats were designed for wrapping text around images, not for page layout. Flexbox and Grid are the modern, purpose-built tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexbox vs Grid: one dimension or two
&lt;/h2&gt;

&lt;p&gt;Both Flexbox and Grid are layout &lt;em&gt;systems&lt;/em&gt; you switch a container into. The single question that tells you which to use: &lt;strong&gt;am I laying things out along one line, or in a grid of rows and columns?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Flexbox&lt;/th&gt;
&lt;th&gt;Grid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dimensions&lt;/td&gt;
&lt;td&gt;One at a time (a row OR a column)&lt;/td&gt;
&lt;td&gt;Two at once (rows AND columns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mental model&lt;/td&gt;
&lt;td&gt;Distribute items along a single axis&lt;/td&gt;
&lt;td&gt;Place items into a defined grid of cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sizing driven by&lt;/td&gt;
&lt;td&gt;Mostly the content itself&lt;/td&gt;
&lt;td&gt;Mostly the container's defined tracks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Navbars, toolbars, button rows, centering one thing, card internals&lt;/td&gt;
&lt;td&gt;Page layouts, image galleries, dashboards, anything with aligned rows + columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turn it on&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;display: flex&lt;/code&gt; on the parent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;display: grid&lt;/code&gt; on the parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule of thumb&lt;/td&gt;
&lt;td&gt;Content arranged in a line → Flexbox&lt;/td&gt;
&lt;td&gt;Layout with a defined structure → Grid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Reach for Flexbox for content arranged in a line; reach for Grid for a true 2D structure.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;They are friends, not rivals:&lt;/strong&gt; Real pages use both: Grid for the overall page skeleton (header / sidebar / main / footer), Flexbox &lt;em&gt;inside&lt;/em&gt; each region to line up its contents. You don't pick one for the whole app, you pick the right one per container.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Build it: a responsive layout
&lt;/h2&gt;

&lt;p&gt;Here is a small but real layout: a centered hero, then a card grid that reflows from three columns to one on narrow screens. Note the very first rule, set &lt;code&gt;box-sizing: border-box&lt;/code&gt; globally so &lt;code&gt;width&lt;/code&gt; finally means the &lt;em&gt;total&lt;/em&gt; width, padding and border included. This one line ends the box-model surprise forever.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;layout.css&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 1. Make width mean total width, everywhere */&lt;/span&gt;
&lt;span class="o"&gt;*,&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* 2. Flexbox: center one thing on both axes */&lt;/span&gt;
&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* main axis: horizontal */&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* cross axis: vertical   */&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* 3. Grid: a responsive card layout, no media query needed */&lt;/span&gt;
&lt;span class="nc"&gt;.cards&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* as many 240px+ columns as fit; they share leftover space */&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto-fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;240px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* width:100% now includes padding + border, no overflow */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#e5e7eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* 4. Flexbox again, inside each card, to stack + space its parts */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic "center a div" problem? Solved in three lines: &lt;code&gt;display: flex; justify-content: center; align-items: center;&lt;/code&gt; on the parent. No magic margins, no negative offsets. And the card grid is fully responsive with &lt;strong&gt;zero media queries&lt;/strong&gt;, &lt;code&gt;auto-fit&lt;/code&gt; plus &lt;code&gt;minmax&lt;/code&gt; lets the browser decide how many columns fit. That is the modern toolset doing the work you used to do by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cascade &amp;amp; specificity
&lt;/h2&gt;

&lt;p&gt;The C in CSS is &lt;strong&gt;Cascading&lt;/strong&gt;. When two rules target the same element and disagree, the browser needs a tiebreaker. It resolves conflicts in this order: &lt;strong&gt;importance&lt;/strong&gt;, then &lt;strong&gt;specificity&lt;/strong&gt;, then &lt;strong&gt;source order&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Importance&lt;/strong&gt;, an &lt;code&gt;!important&lt;/code&gt; declaration beats a normal one. (Use this almost never; see the mistakes below.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specificity&lt;/strong&gt;, a score based on the selector. Inline &lt;code&gt;style=""&lt;/code&gt; beats an &lt;code&gt;#id&lt;/code&gt; beats a &lt;code&gt;.class&lt;/code&gt; beats a plain &lt;code&gt;tag&lt;/code&gt;. Roughly: ids are worth more than classes, classes more than tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source order&lt;/strong&gt;, if specificity ties, the rule written &lt;em&gt;last&lt;/em&gt; wins. This is why the order of your stylesheet matters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A concrete tiebreak: a &lt;code&gt;.btn&lt;/code&gt; rule and a &lt;code&gt;button&lt;/code&gt; rule both set colour. The &lt;code&gt;.btn&lt;/code&gt; class wins because a class outranks a tag, regardless of which one you wrote first. Most "my CSS isn't applying!" moments are a more specific rule quietly overriding yours. The fix is almost never &lt;code&gt;!important&lt;/code&gt;, it's understanding &lt;em&gt;which&lt;/em&gt; selector is winning and matching or beating it cleanly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Keep specificity low and flat:&lt;/strong&gt; Prefer single classes (&lt;code&gt;.card-title&lt;/code&gt;) over deep chains (&lt;code&gt;section.content div.card h2.title&lt;/code&gt;). Flat, low-specificity selectors are easy to override later. Deep, high-specificity ones start a war that ends in &lt;code&gt;!important&lt;/code&gt;, and a stylesheet nobody can safely change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;box-sizing: border-box&lt;/code&gt;.&lt;/strong&gt; Without it, adding padding makes elements &lt;em&gt;wider&lt;/em&gt; than the &lt;code&gt;width&lt;/code&gt; you set, so they overflow their container. Set it globally once (as in the code above) and never think about box-model math again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fighting specificity with &lt;code&gt;!important&lt;/code&gt;.&lt;/strong&gt; It feels like a fix; it's a debt. Now the &lt;em&gt;only&lt;/em&gt; way to override that rule is another &lt;code&gt;!important&lt;/code&gt;, and you've started an arms race. Lower your selector specificity instead, &lt;code&gt;!important&lt;/code&gt; should be a last resort, not a habit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;float&lt;/code&gt; for layout.&lt;/strong&gt; Floats were built to wrap text around images. Using them to put boxes side by side leads to collapsed parents, clearfix hacks, and fragile layouts. Use Flexbox or Grid, they were designed for exactly this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing padding and margin.&lt;/strong&gt; Padding adds space &lt;em&gt;inside&lt;/em&gt; (and inherits the background); margin adds space &lt;em&gt;outside&lt;/em&gt; (always transparent). Reaching for the wrong one is why backgrounds bleed or gaps refuse to appear where you want them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical margins that mysteriously collapse.&lt;/strong&gt; Adjacent top/bottom margins merge into the larger of the two rather than adding up. If a gap is half what you expected, margin collapsing is the usual suspect, &lt;code&gt;gap&lt;/code&gt; inside a flex/grid container sidesteps it entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every element is a box: &lt;strong&gt;content → padding → border → margin&lt;/strong&gt;, nested like a parcel.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;box-sizing: border-box&lt;/code&gt; globally so &lt;code&gt;width&lt;/code&gt; means the &lt;em&gt;total&lt;/em&gt; width, this kills the #1 layout bug.&lt;/li&gt;
&lt;li&gt;Padding is inside (inherits background); margin is outside (always transparent).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normal flow&lt;/strong&gt; is the default: block elements stack, inline elements sit in a line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexbox = one dimension&lt;/strong&gt; (a row or a column). &lt;strong&gt;Grid = two dimensions&lt;/strong&gt; (rows and columns). Use both, per container.&lt;/li&gt;
&lt;li&gt;The cascade resolves conflicts by &lt;strong&gt;importance → specificity → source order&lt;/strong&gt;; keep specificity low and flat.&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;!important&lt;/code&gt; and floats-for-layout, they're the debts that cost you hours later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have the four mental models that make CSS predictable. The natural next steps are understanding &lt;em&gt;why&lt;/em&gt; the browser draws these boxes the way it does, and then how to organize CSS once a project grows past a single file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read &lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;How the Browser Renders a Page&lt;/a&gt; to see how the DOM, CSSOM, layout, and paint turn your boxes into pixels, and why some CSS changes are far cheaper than others.&lt;/li&gt;
&lt;li&gt;Read &lt;a href="https://thesimplifiedtech.com/blog/component-architecture-and-design-systems" rel="noopener noreferrer"&gt;Component Architecture &amp;amp; Design Systems&lt;/a&gt; for how to keep specificity low and styles reusable as your app scales beyond one stylesheet.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer path&lt;/a&gt; for the foundation-to-senior track these articles belong to.&lt;/li&gt;
&lt;li&gt;Practice deliberately: rebuild a layout you admire using only Grid for the page skeleton and Flexbox inside each region. Nothing cements these models faster than shipping one real layout.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/css-fundamentals-the-box-model-and-layout" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>How the Browser Renders a Page</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Thu, 08 Oct 2026 17:40:11 +0000</pubDate>
      <link>https://dev.to/sri2614/how-the-browser-renders-a-page-4ckc</link>
      <guid>https://dev.to/sri2614/how-the-browser-renders-a-page-4ckc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Trace HTML bytes to pixels through the critical rendering path: DOM, CSSOM, layout, paint, composite. Once you know what blocks rendering and why &lt;strong&gt;reflows&lt;/strong&gt; are expensive, you can stop layout thrashing and fix the real performance killers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You hit Enter, then what?&lt;/li&gt;
&lt;li&gt;One sentence, then a picture&lt;/li&gt;
&lt;li&gt;The critical rendering path&lt;/li&gt;
&lt;li&gt;What happens at each stage, and what slows it down&lt;/li&gt;
&lt;li&gt;What blocks rendering&lt;/li&gt;
&lt;li&gt;Reflow vs repaint vs composite (and layout thrashing)&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You hit Enter, then what?
&lt;/h2&gt;

&lt;p&gt;You type a URL, hit Enter, and a fraction of a second later a fully styled page appears. It feels like one instant event. It isn't. Between the server sending back a blob of text and you seeing pixels, the browser runs a precise assembly line called the &lt;strong&gt;critical rendering path&lt;/strong&gt;. Every stage depends on the one before it, and any stage can stall the whole pipeline.&lt;/p&gt;

&lt;p&gt;Most performance problems, janky scrolling, a button that lags when you click it, a page that sits blank for a beat too long, are really just one of these stages doing more work than it should. Once you can name the stages, you can name the bug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Frontend developers who can write HTML, CSS, and JavaScript but have never looked under the hood at how they become pixels. No prior browser-internals knowledge needed. If you have ever wondered why "just adding a class" can cause a stutter, this is for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One sentence, then a picture
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Rendering is the process of turning your HTML and CSS into a tree the browser understands, working out where everything goes, and then drawing it, in that order, every time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of it like assembling flat-pack furniture. The HTML is the instruction booklet that lists every part and how they nest together. The CSS is the parts list that says which panel is which colour, which screw is which size. You cannot start screwing things together until you have read &lt;strong&gt;both&lt;/strong&gt; the instructions and checked the parts. Only then can you lay the pieces out on the floor (layout), paint or stain them (paint), and finally slot the finished sections into place to form the wardrobe (composite).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The instruction booklet (how parts nest)&lt;/td&gt;
&lt;td&gt;HTML parsed into the DOM tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The parts list (which panel, which colour)&lt;/td&gt;
&lt;td&gt;CSS parsed into the CSSOM tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading instructions + parts together&lt;/td&gt;
&lt;td&gt;DOM + CSSOM merged into the render tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laying pieces out on the floor&lt;/td&gt;
&lt;td&gt;Layout, computing position and size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Painting and staining each piece&lt;/td&gt;
&lt;td&gt;Paint, filling in pixels, colours, shadows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slotting finished sections together&lt;/td&gt;
&lt;td&gt;Composite, stacking layers into the final frame&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Building a page is building flat-pack furniture.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The critical rendering path
&lt;/h2&gt;

&lt;p&gt;Here is the whole pipeline. Bytes come in on the left; pixels come out on the right. The browser never skips a step, and it processes them strictly left to right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5faHRtbCgiSFRNTDxici8-U291cmNlIGJ5dGVzIikKICBjbGFzcyBuX2h0bWwgZXh0ZXJuYWw7CiAgbl9kb20oIkRPTTxici8-T2JqZWN0IHRyZWUiKQogIGNsYXNzIG5fZG9tIGNvbXB1dGU7CiAgbl9yZW5kZXIoIlJlbmRlciBUcmVlPGJyLz5WaXNpYmxlIG5vZGVzIikKICBjbGFzcyBuX3JlbmRlciBkYXRhOwogIG5fbGF5b3V0KCJMYXlvdXQ8YnIvPlBvc2l0aW9uICsgc2l6ZSIpCiAgY2xhc3Mgbl9sYXlvdXQgY29tcHV0ZTsKICBuX3BhaW50KCJQYWludDxici8-RmlsbCBwaXhlbHMiKQogIGNsYXNzIG5fcGFpbnQgY29tcHV0ZTsKICBuX2NvbXBvc2l0ZSgiQ29tcG9zaXRlPGJyLz5GaW5hbCBmcmFtZSIpCiAgY2xhc3Mgbl9jb21wb3NpdGUgY2xpZW50OwogIG5fY3NzKCJDU1M8YnIvPlNvdXJjZSBieXRlcyIpCiAgY2xhc3Mgbl9jc3MgZXh0ZXJuYWw7CiAgbl9jc3NvbSgiQ1NTT008YnIvPlN0eWxlIHRyZWUiKQogIGNsYXNzIG5fY3Nzb20gY29tcHV0ZTsKICBuX2h0bWwgLS0-IG5fZG9tCiAgbl9jc3MgLS0-IG5fY3Nzb20KICBuX2RvbSAtLT4gbl9yZW5kZXIKICBuX2Nzc29tIC0uLT4gbl9yZW5kZXIKICBuX3JlbmRlciAtLT4gbl9sYXlvdXQKICBuX2xheW91dCAtLT4gbl9wYWludAogIG5fcGFpbnQgLS0-IG5fY29tcG9zaXRl%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5faHRtbCgiSFRNTDxici8-U291cmNlIGJ5dGVzIikKICBjbGFzcyBuX2h0bWwgZXh0ZXJuYWw7CiAgbl9kb20oIkRPTTxici8-T2JqZWN0IHRyZWUiKQogIGNsYXNzIG5fZG9tIGNvbXB1dGU7CiAgbl9yZW5kZXIoIlJlbmRlciBUcmVlPGJyLz5WaXNpYmxlIG5vZGVzIikKICBjbGFzcyBuX3JlbmRlciBkYXRhOwogIG5fbGF5b3V0KCJMYXlvdXQ8YnIvPlBvc2l0aW9uICsgc2l6ZSIpCiAgY2xhc3Mgbl9sYXlvdXQgY29tcHV0ZTsKICBuX3BhaW50KCJQYWludDxici8-RmlsbCBwaXhlbHMiKQogIGNsYXNzIG5fcGFpbnQgY29tcHV0ZTsKICBuX2NvbXBvc2l0ZSgiQ29tcG9zaXRlPGJyLz5GaW5hbCBmcmFtZSIpCiAgY2xhc3Mgbl9jb21wb3NpdGUgY2xpZW50OwogIG5fY3NzKCJDU1M8YnIvPlNvdXJjZSBieXRlcyIpCiAgY2xhc3Mgbl9jc3MgZXh0ZXJuYWw7CiAgbl9jc3NvbSgiQ1NTT008YnIvPlN0eWxlIHRyZWUiKQogIGNsYXNzIG5fY3Nzb20gY29tcHV0ZTsKICBuX2h0bWwgLS0-IG5fZG9tCiAgbl9jc3MgLS0-IG5fY3Nzb20KICBuX2RvbSAtLT4gbl9yZW5kZXIKICBuX2Nzc29tIC0uLT4gbl9yZW5kZXIKICBuX3JlbmRlciAtLT4gbl9sYXlvdXQKICBuX2xheW91dCAtLT4gbl9wYWludAogIG5fcGFpbnQgLS0-IG5fY29tcG9zaXRl%3FbgColor%3D0d1017%26type%3Dpng" alt="The critical rendering path: from source files to pixels on screen." width="1041" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The critical rendering path: from source files to pixels on screen.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;HTML → DOM&lt;/strong&gt;: The browser reads the HTML bytes, decodes them into characters, tokenizes the tags, and builds the &lt;strong&gt;Document Object Model&lt;/strong&gt;, a tree of nodes where every element, attribute, and text node is an object you can read and change from JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS → CSSOM&lt;/strong&gt;: In parallel, every stylesheet (external, internal, and inline) is parsed into the &lt;strong&gt;CSS Object Model&lt;/strong&gt;, a tree of style rules. Like the DOM, it is a tree, because styles cascade and inherit down from parents to children.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOM + CSSOM → Render Tree&lt;/strong&gt;: The two trees are merged. The render tree contains only what will actually be drawn: nodes with &lt;code&gt;display: none&lt;/code&gt; are dropped entirely, and each remaining node carries its computed styles. &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, and hidden elements never make it in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout (a.k.a. reflow)&lt;/strong&gt;: The browser walks the render tree and calculates the exact geometry of every node, its width, height, and x/y position on the page. This is where percentages, flexbox, and grid get resolved into real pixel boxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paint&lt;/strong&gt;: With geometry known, the browser fills in the actual pixels: text glyphs, colours, borders, shadows, gradients, images. The output is a set of draw commands, often split across several layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite&lt;/strong&gt;: Finally the painted layers are stacked in the right order, respecting z-index, transforms, and opacity, and handed to the GPU to assemble the single frame you see. Smooth animations live here.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What happens at each stage, and what slows it down
&lt;/h2&gt;

&lt;p&gt;Each stage has its own cost profile. Knowing which stage your change touches tells you how expensive it will be. Here is the cheat sheet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;What makes it slow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DOM build&lt;/td&gt;
&lt;td&gt;HTML parsed into a node tree&lt;/td&gt;
&lt;td&gt;Huge or deeply nested markup; thousands of elements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSSOM build&lt;/td&gt;
&lt;td&gt;Stylesheets parsed into style rules&lt;/td&gt;
&lt;td&gt;Large CSS files; complex, deeply nested selectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render tree&lt;/td&gt;
&lt;td&gt;Visible DOM nodes get computed styles&lt;/td&gt;
&lt;td&gt;Mostly proportional to DOM + CSSOM size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout&lt;/td&gt;
&lt;td&gt;Geometry of every box computed&lt;/td&gt;
&lt;td&gt;Touching layout-affecting properties; large DOM; reading layout mid-write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paint&lt;/td&gt;
&lt;td&gt;Pixels filled per layer&lt;/td&gt;
&lt;td&gt;Large paint areas; expensive effects (shadows, blur, gradients)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite&lt;/td&gt;
&lt;td&gt;Layers stacked on the GPU&lt;/td&gt;
&lt;td&gt;Too many layers; very large layers eating GPU memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The six stages, what they do, and what makes them slow.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What blocks rendering
&lt;/h2&gt;

&lt;p&gt;Two resource types can freeze the pipeline: &lt;strong&gt;CSS&lt;/strong&gt; and &lt;strong&gt;JavaScript&lt;/strong&gt;. CSS is render-blocking by default, the browser will not build the render tree (and therefore will not paint) until it has the CSSOM, because painting with the wrong styles would cause an ugly flash. JavaScript is parser-blocking: a plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; stops HTML parsing dead while it downloads and executes, because the script might call &lt;code&gt;document.write&lt;/code&gt; or read the DOM.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- BLOCKS: parser stops here until app.js downloads AND runs --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- BETTER: download in parallel, run after HTML is parsed --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/app.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- For independent scripts (analytics): run whenever ready --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/analytics.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- CSS is render-blocking: the page won't paint until this loads --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/styles.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defer&lt;/code&gt;&lt;/strong&gt;, download the script in the background while parsing continues, then execute it in order, just before &lt;code&gt;DOMContentLoaded&lt;/code&gt;. The right default for app code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;async&lt;/code&gt;&lt;/strong&gt;, download in the background, then execute the moment it arrives, interrupting parsing. Good for independent third-party scripts that do not touch your DOM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No attribute&lt;/strong&gt;, fully blocking. Avoid in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; unless the script genuinely must run before the page is parsed.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why CSS in the  is still correct:&lt;/strong&gt; It is tempting to think render-blocking CSS is bad, so move it down. Do not. You want CSS discovered early so the CSSOM is ready by the time the DOM is. Instead, keep the critical CSS small and load non-essential styles separately. Block early on a little, not late on a lot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reflow vs repaint vs composite (and layout thrashing)
&lt;/h2&gt;

&lt;p&gt;After the first render, the page is not frozen, JavaScript and user interaction keep changing it. The cost of a change depends on &lt;strong&gt;how far back up the pipeline it forces the browser to go.&lt;/strong&gt; This is the single most useful performance idea in this whole article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reflow (layout)&lt;/strong&gt; is the most expensive. Change something geometric, &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;font-size&lt;/code&gt;, adding a DOM node, and the browser must recompute geometry, then repaint, then composite. The whole tail of the pipeline reruns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repaint&lt;/strong&gt; is cheaper. Change only appearance, &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;background&lt;/code&gt;, &lt;code&gt;box-shadow&lt;/code&gt;, &lt;code&gt;visibility&lt;/code&gt;, and geometry is untouched, so the browser skips layout but still repaints and composites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite-only&lt;/strong&gt; is cheapest. Animate &lt;code&gt;transform&lt;/code&gt; or &lt;code&gt;opacity&lt;/code&gt; and the browser can skip both layout and paint, just re-stacking existing layers on the GPU. This is why smooth 60fps animations stick to &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Layout thrashing&lt;/strong&gt; is the classic trap. The browser is smart enough to batch your style writes and reflow once, &lt;em&gt;unless&lt;/em&gt; you read a layout value in between. Reading something like &lt;code&gt;offsetHeight&lt;/code&gt; forces the browser to flush all pending changes and reflow &lt;em&gt;right now&lt;/em&gt; so it can give you an accurate answer. Do that inside a loop and you force a reflow on every single iteration.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;thrash.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Layout thrashing: read -&amp;gt; write -&amp;gt; read -&amp;gt; write...&lt;/span&gt;
&lt;span class="c1"&gt;// Each offsetWidth read forces a synchronous reflow.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// READ (forces reflow)&lt;/span&gt;
  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// WRITE (invalidates layout)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Batch: read everything first, then write everything.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// all READS&lt;/span&gt;
&lt;span class="nx"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// all WRITES&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Render-blocking scripts in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/strong&gt; A plain &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; before your content stalls parsing and delays first paint. Add &lt;code&gt;defer&lt;/code&gt;, or move it to the end of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout thrashing in loops.&lt;/strong&gt; Interleaving DOM reads (&lt;code&gt;offsetTop&lt;/code&gt;, &lt;code&gt;getBoundingClientRect&lt;/code&gt;) and writes forces a reflow per iteration. Batch all reads, then all writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animating layout properties.&lt;/strong&gt; Animating &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, or &lt;code&gt;margin&lt;/code&gt; reflows every frame and drops your frame rate. Animate &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A huge DOM.&lt;/strong&gt; Tens of thousands of nodes make every reflow and style recalculation slow. Virtualize long lists and prune unnecessary wrapper elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One giant CSS file.&lt;/strong&gt; All of it is render-blocking. Ship the critical styles inline or small, and defer the rest so first paint is not held hostage.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in six lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser runs a fixed pipeline: HTML→DOM, CSS→CSSOM, render tree, layout, paint, composite.&lt;/li&gt;
&lt;li&gt;CSS is render-blocking; plain scripts are parser-blocking. Use &lt;code&gt;defer&lt;/code&gt; for app code, &lt;code&gt;async&lt;/code&gt; for independent scripts.&lt;/li&gt;
&lt;li&gt;Reflow (layout) is the most expensive change, repaint is cheaper, composite-only is cheapest.&lt;/li&gt;
&lt;li&gt;Animate &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; to stay on the cheap composite-only path.&lt;/li&gt;
&lt;li&gt;Layout thrashing, reading a layout value between writes, forces a reflow per read. Batch reads, then writes.&lt;/li&gt;
&lt;li&gt;Most jank is one stage doing too much work. Name the stage, fix the bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Now that you can see the pipeline, the next step is measuring it on real pages and understanding the layout model that feeds the layout stage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/core-web-vitals-and-frontend-performance" rel="noopener noreferrer"&gt;Core Web Vitals &amp;amp; Frontend Performance&lt;/a&gt;, turn this mental model into the metrics Google actually measures.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/css-fundamentals-the-box-model-and-layout" rel="noopener noreferrer"&gt;CSS Fundamentals: The Box Model and Layout&lt;/a&gt;, master the box model that the layout stage resolves into pixels.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer path&lt;/a&gt; to build on these foundations step by step.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>What Is a Frontend Engineer?</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:37:03 +0000</pubDate>
      <link>https://dev.to/sri2614/what-is-a-frontend-engineer-2d20</link>
      <guid>https://dev.to/sri2614/what-is-a-frontend-engineer-2d20</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Frontend engineering is far more than styling: it owns the browser runtime, interaction, and the human-facing layer. The takeaway is a clear map of what the role actually owns versus the backend, and the modern skills it really demands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You can see the work, and that is the hard part&lt;/li&gt;
&lt;li&gt;A one-line definition (and a kitchen)&lt;/li&gt;
&lt;li&gt;The browser is the runtime&lt;/li&gt;
&lt;li&gt;Frontend vs backend: who owns what&lt;/li&gt;
&lt;li&gt;The modern skill set (beyond "just styling")&lt;/li&gt;
&lt;li&gt;Common misconceptions&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You can see the work, and that is the hard part
&lt;/h2&gt;

&lt;p&gt;Open any app you used today. The login screen, the menu that slides in, the spinner while a page loads, the form that turns red when you mistype your email, every pixel a human looks at or touches was built by a &lt;strong&gt;frontend engineer&lt;/strong&gt;. And because everyone can &lt;em&gt;see&lt;/em&gt; it, everyone has an opinion on it. That is the strange pressure of the role: your work is the most visible thing in the entire company, and the least forgiving.&lt;/p&gt;

&lt;p&gt;The catch is that what users see is the smallest part of the job. Making a button look right is an afternoon. Making it stay usable across phones and laptops, load fast on a slow train, work for someone using a screen reader, and not break when the server returns an error, that is the actual craft. People hear "frontend" and think "styling." The reality is closer to engineering a live application that runs on a machine you do not control: the user's browser.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Anyone curious about what a frontend engineer actually does, career switchers, students, backend devs eyeing the other side, or hiring managers writing a job spec. No coding experience assumed. By the end you will know what the role owns, where it stops, and the skills that separate a junior from a senior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A one-line definition (and a kitchen)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A frontend engineer builds the user-facing layer of an application, the interface that runs inside the browser, using HTML, CSS, JavaScript, and frameworks, and connects it to backend services over the network.&lt;/p&gt;

&lt;p&gt;Working definition&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is dense, so picture a restaurant instead. The &lt;strong&gt;kitchen&lt;/strong&gt; is the backend: the chefs, the inventory, the recipes, the food safety rules. Customers never see it, and that is the point, it is where the real cooking, storage, and logic happen.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dining room and front-of-house&lt;/strong&gt; is the frontend. It is the menu you read, the table layout, the lighting, the waiter who takes your order and brings it back. The front-of-house does not grow the vegetables, but if the menu is confusing, the lighting is harsh, or the waiter forgets your order, you have a bad night no matter how good the kitchen is. The frontend engineer designs and runs that dining room: every interaction between the human and the system passes through it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The dining room you sit in&lt;/td&gt;
&lt;td&gt;The UI rendered in the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The menu you read and choose from&lt;/td&gt;
&lt;td&gt;HTML, the content and structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighting, table layout, decor&lt;/td&gt;
&lt;td&gt;CSS, the visual styling and layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The waiter taking and relaying your order&lt;/td&gt;
&lt;td&gt;JavaScript, the behaviour and interactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The kitchen cooking and storing food&lt;/td&gt;
&lt;td&gt;The backend: servers, databases, business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The waiter walking orders to the kitchen&lt;/td&gt;
&lt;td&gt;API calls over the network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The restaurant maps almost one-to-one onto how a web app is built.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser is the runtime
&lt;/h2&gt;

&lt;p&gt;Here is the idea that reframes everything: the frontend engineer does not control the computer the code runs on. A backend engineer deploys to servers they configure. The frontend ships code to the user's &lt;strong&gt;browser&lt;/strong&gt;, Chrome, Safari, Firefox, on a flagship phone or a five-year-old budget Android, and that browser is the runtime. It downloads three kinds of files and assembles a living interface out of them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML&lt;/strong&gt; is the &lt;em&gt;structure&lt;/em&gt;, the headings, paragraphs, buttons, and inputs. It is the skeleton and the content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS&lt;/strong&gt; is the &lt;em&gt;presentation&lt;/em&gt;, colors, spacing, fonts, and how the layout reflows from a wide laptop to a narrow phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; is the &lt;em&gt;behaviour&lt;/em&gt;, what happens when you click, type, or scroll, and how the page fetches fresh data without a full reload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fc2VydmVyKCJXZWIgU2VydmVyIC8gQ0ROPGJyLz5TZW5kcyBIVE1MLCBDU1MsIEpTIikKICBjbGFzcyBuX3NlcnZlciBleHRlcm5hbDsKICBuX2Jyb3dzZXIoIkJyb3dzZXI8YnIvPlRoZSBydW50aW1lIikKICBjbGFzcyBuX2Jyb3dzZXIgY2xpZW50OwogIG5fdWkoIlJlbmRlcmVkIFVJPGJyLz5XaGF0IHRoZSB1c2VyIHRvdWNoZXMiKQogIGNsYXNzIG5fdWkgZWRnZTsKICBuX3VzZXIoIlVzZXI8YnIvPkNsaWNrcywgdHlwZXMsIHNjcm9sbHMiKQogIGNsYXNzIG5fdXNlciBjbGllbnQ7CiAgbl9hcGkoIkJhY2tlbmQgQVBJPGJyLz5EYXRhICYgYnVzaW5lc3MgbG9naWMiKQogIGNsYXNzIG5fYXBpIGNvbXB1dGU7CiAgbl9kYigiRGF0YWJhc2U8YnIvPlN0b3JlZCBkYXRhIikKICBjbGFzcyBuX2RiIGRhdGE7CiAgbl9zZXJ2ZXIgLS0-fCIxLiBkb3dubG9hZCBmaWxlcyJ8IG5fYnJvd3NlcgogIG5fYnJvd3NlciAtLT58IjIuIGJ1aWxkIFVJInwgbl91aQogIG5fdWkgLS0-fCIzLiBpbnRlcmFjdCJ8IG5fdXNlcgogIG5fdWkgLS4tPnwiNC4gZmV0Y2ggZGF0YSJ8IG5fYXBpCiAgbl9hcGkgLS4tPnwicXVlcnkifCBuX2Ri%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fc2VydmVyKCJXZWIgU2VydmVyIC8gQ0ROPGJyLz5TZW5kcyBIVE1MLCBDU1MsIEpTIikKICBjbGFzcyBuX3NlcnZlciBleHRlcm5hbDsKICBuX2Jyb3dzZXIoIkJyb3dzZXI8YnIvPlRoZSBydW50aW1lIikKICBjbGFzcyBuX2Jyb3dzZXIgY2xpZW50OwogIG5fdWkoIlJlbmRlcmVkIFVJPGJyLz5XaGF0IHRoZSB1c2VyIHRvdWNoZXMiKQogIGNsYXNzIG5fdWkgZWRnZTsKICBuX3VzZXIoIlVzZXI8YnIvPkNsaWNrcywgdHlwZXMsIHNjcm9sbHMiKQogIGNsYXNzIG5fdXNlciBjbGllbnQ7CiAgbl9hcGkoIkJhY2tlbmQgQVBJPGJyLz5EYXRhICYgYnVzaW5lc3MgbG9naWMiKQogIGNsYXNzIG5fYXBpIGNvbXB1dGU7CiAgbl9kYigiRGF0YWJhc2U8YnIvPlN0b3JlZCBkYXRhIikKICBjbGFzcyBuX2RiIGRhdGE7CiAgbl9zZXJ2ZXIgLS0-fCIxLiBkb3dubG9hZCBmaWxlcyJ8IG5fYnJvd3NlcgogIG5fYnJvd3NlciAtLT58IjIuIGJ1aWxkIFVJInwgbl91aQogIG5fdWkgLS0-fCIzLiBpbnRlcmFjdCJ8IG5fdXNlcgogIG5fdWkgLS4tPnwiNC4gZmV0Y2ggZGF0YSJ8IG5fYXBpCiAgbl9hcGkgLS4tPnwicXVlcnkifCBuX2Ri%3FbgColor%3D0d1017%26type%3Dpng" alt="The browser downloads HTML, CSS, and JS, assembles the UI the user touches, and talks to backend APIs for data." width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The browser downloads HTML, CSS, and JS, assembles the UI the user touches, and talks to backend APIs for data.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The browser requests the page&lt;/strong&gt;: You type a URL or click a link. The browser asks a server (often a CDN near you) for the page and receives HTML, CSS, and JavaScript files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It parses HTML into structure&lt;/strong&gt;: The browser reads the HTML and builds an in-memory tree of every element, the DOM. This is the skeleton the rest hangs on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It applies CSS and paints&lt;/strong&gt;: Styles are matched to elements, the layout is calculated, and the browser paints actual pixels: colors, spacing, fonts, position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript brings it alive&lt;/strong&gt;: Scripts run, attach to buttons and inputs, and wait for events. Now clicking, typing, and scrolling actually do something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI talks to the backend&lt;/strong&gt;: When you submit a form or load more results, JavaScript calls a backend API over the network, gets data back, and updates the screen, no full reload needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frontend vs backend: who owns what
&lt;/h2&gt;

&lt;p&gt;The clearest way to understand the role is by its boundary. Frontend and backend are two halves of the same app that meet at the &lt;strong&gt;API&lt;/strong&gt;, the contract for what data gets sent and received. Everything on the user's side of that contract is the frontend's responsibility; everything behind it belongs to the backend.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Frontend owns&lt;/th&gt;
&lt;th&gt;Backend owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where the code runs&lt;/td&gt;
&lt;td&gt;The user's browser&lt;/td&gt;
&lt;td&gt;Servers the team controls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core languages&lt;/td&gt;
&lt;td&gt;HTML, CSS, JavaScript / TypeScript&lt;/td&gt;
&lt;td&gt;Python, Go, Java, Node, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main job&lt;/td&gt;
&lt;td&gt;Render the UI, handle interaction&lt;/td&gt;
&lt;td&gt;Store data, run business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Login" feature&lt;/td&gt;
&lt;td&gt;The form, validation, error states, redirect&lt;/td&gt;
&lt;td&gt;Check the password, issue a session token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance focus&lt;/td&gt;
&lt;td&gt;Load time, smooth interaction, bundle size&lt;/td&gt;
&lt;td&gt;Query speed, throughput, uptime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Display it, never the source of truth&lt;/td&gt;
&lt;td&gt;The source of truth (databases)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security focus&lt;/td&gt;
&lt;td&gt;Input handling, XSS, safe rendering&lt;/td&gt;
&lt;td&gt;Auth, authorization, data protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The same feature, split across the API boundary.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The grey zone is real:&lt;/strong&gt; Plenty of work straddles the line, form validation often lives in both places, and a "full-stack" engineer crosses it daily. The table is a map of ownership, not a wall. Knowing the boundary makes you a better teammate even when you step over it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The modern skill set (beyond "just styling")
&lt;/h2&gt;

&lt;p&gt;If you only learn HTML, CSS, and a little JavaScript, you can build a page. To build a &lt;em&gt;product&lt;/em&gt; people rely on, the modern role asks for more. Here is the real surface area of the job today.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The three core languages&lt;/strong&gt;, deeply, not just "making it look right" in CSS but understanding layout, the box model, and responsive design, and writing JavaScript that handles real logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A framework&lt;/strong&gt; like React, Vue, Svelte, or Angular, the industry builds large UIs as reusable &lt;strong&gt;components&lt;/strong&gt;, not hand-written pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working with APIs&lt;/strong&gt;, fetching data, handling loading and error states, and showing the right thing while the user waits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive and cross-browser&lt;/strong&gt;, one codebase that works on phones, tablets, and laptops, across different browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility (a11y)&lt;/strong&gt;, building so people using keyboards and screen readers can use the app too. This is a baseline, not a bonus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;, keeping bundles small and interactions smooth, because users abandon slow pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling and Git&lt;/strong&gt;, build tools, package managers, and version control are part of the daily workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration with design&lt;/strong&gt;, turning mockups into pixel-faithful, interactive interfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common misconceptions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Frontend is just CSS / making things pretty."&lt;/strong&gt; Styling is one skill among many. The hard parts are state, data flow, performance, and accessibility, all logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"It's the easy half of web dev."&lt;/strong&gt; It is &lt;em&gt;different&lt;/em&gt;, not easy. You debug across browsers and devices you do not control, which is its own kind of hard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"You don't need to understand the backend."&lt;/strong&gt; You consume its APIs constantly. Knowing how the backend thinks makes you faster and your bug reports sharper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"A framework is all you need to learn."&lt;/strong&gt; Frameworks come and go. Solid HTML, CSS, and JavaScript fundamentals outlast every one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Design and frontend are the same job."&lt;/strong&gt; Designers decide &lt;em&gt;what it should look like&lt;/em&gt;; frontend engineers &lt;em&gt;build it so it works&lt;/em&gt;, and the best ones speak both languages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in six lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A frontend engineer builds the user-facing layer, the interface that runs in the browser.&lt;/li&gt;
&lt;li&gt;HTML is structure, CSS is presentation, JavaScript is behaviour.&lt;/li&gt;
&lt;li&gt;The browser is the runtime: you ship code to a machine you do not control.&lt;/li&gt;
&lt;li&gt;Frontend and backend meet at the API; everything on the user's side is yours.&lt;/li&gt;
&lt;li&gt;The modern role is engineering, not styling: components, data, performance, accessibility.&lt;/li&gt;
&lt;li&gt;Fundamentals outlast frameworks, learn the three core languages deeply.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Now that you know what the role is, the next step is understanding the runtime it lives in and the three languages it is built on. Start with how the browser turns files into pixels, then go deep on each core language.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/how-the-browser-renders-a-page" rel="noopener noreferrer"&gt;How the Browser Renders a Page&lt;/a&gt;, what really happens between request and pixels.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/css-fundamentals-the-box-model-and-layout" rel="noopener noreferrer"&gt;CSS Fundamentals: The Box Model and Layout&lt;/a&gt;, the model behind every layout you will ever build.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/javascript-for-the-browser" rel="noopener noreferrer"&gt;JavaScript for the Browser&lt;/a&gt;, the language that makes the page do things.&lt;/li&gt;
&lt;li&gt;Ready to commit? Follow the structured &lt;a href="https://thesimplifiedtech.com/career-paths/frontend-engineer" rel="noopener noreferrer"&gt;Frontend Engineer career path&lt;/a&gt; from foundation to senior.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/what-is-a-frontend-engineer" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>API Design: Evolution &amp; Versioning</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Tue, 06 Oct 2026 17:00:14 +0000</pubDate>
      <link>https://dev.to/sri2614/api-design-evolution-versioning-49f3</link>
      <guid>https://dev.to/sri2614/api-design-evolution-versioning-49f3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; The rule that keeps clients alive: you can add fields, never remove or rename them. Make changes additive, version only for true breaks, run side-by-side with a real deprecation policy, and let &lt;strong&gt;contract testing&lt;/strong&gt; catch breakage before users do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The day one field rename took down every phone&lt;/li&gt;
&lt;li&gt;The one rule: you can add, you cannot take away&lt;/li&gt;
&lt;li&gt;The lifecycle: v1 and v2 live side by side&lt;/li&gt;
&lt;li&gt;What is safe vs what breaks&lt;/li&gt;
&lt;li&gt;Versioning strategies: where the version lives&lt;/li&gt;
&lt;li&gt;Code: an additive change done right&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The day one field rename took down every phone
&lt;/h2&gt;

&lt;p&gt;It was a one-line change. A field called &lt;code&gt;user_name&lt;/code&gt; had always bothered the team, it should have been &lt;code&gt;username&lt;/code&gt;, one word, like the rest of the schema. So someone renamed it. The pull request was tiny, the tests were green, the deploy was clean. Twenty minutes later, support lit up: the mobile app was showing blank profiles for &lt;strong&gt;every&lt;/strong&gt; user on &lt;strong&gt;every&lt;/strong&gt; version of the app, all at once.&lt;/p&gt;

&lt;p&gt;Nothing was down. The server returned 200. The database was fine. The problem was that three years of shipped mobile binaries were all reading &lt;code&gt;response.user_name&lt;/code&gt;, and that key had simply vanished. You cannot push a hotfix to a phone in the App Store review queue. The fix was to redeploy the old field, and then live with &lt;code&gt;user_name&lt;/code&gt; forever.&lt;/p&gt;

&lt;p&gt;That is the whole discipline of API evolution in one story: &lt;strong&gt;the moment someone depends on your shape, the shape is a promise.&lt;/strong&gt; This article is about keeping that promise while still letting your API grow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Backend engineers who own an API that real clients already consume, mobile apps, partner integrations, other internal services. If you have ever been afraid to change a response because you do not know who reads it, this is for you. We build on &lt;a href="https://thesimplifiedtech.com/blog/rest-api-design" rel="noopener noreferrer"&gt;REST API Design&lt;/a&gt; and assume you know what a JSON response looks like.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one rule: you can add, you cannot take away
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;You can add to an API. You cannot take away from it. Every field you remove and every name you change is a promise broken to someone who already trusted the old shape.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is sometimes called the &lt;strong&gt;robustness principle&lt;/strong&gt; applied to APIs, and it has a clean asymmetry. Adding a new optional field, a new endpoint, or a new enum value that old clients can ignore is safe, nobody was depending on the absence of a thing. Removing a field, renaming it, making an optional field required, or changing its type is unsafe, somebody, somewhere, is reading exactly what you just changed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A contract both parties already signed&lt;/td&gt;
&lt;td&gt;Your published API response shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding a new clause both sides agree to later&lt;/td&gt;
&lt;td&gt;Adding a new optional field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quietly deleting a clause the other party relies on&lt;/td&gt;
&lt;td&gt;Removing or renaming a field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A signed amendment with a start date&lt;/td&gt;
&lt;td&gt;A new API version, announced and dated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A notice period before the old terms expire&lt;/td&gt;
&lt;td&gt;A deprecation window before sunset&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;An API is a signed contract, not a private implementation detail.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The mental shift: your API is not your code. Your code is yours to refactor freely. Your API is a contract you co-signed with every client. You do not get to edit a signed contract unilaterally, you either add an amendment everyone accepts, or you issue a new version and give people time to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle: v1 and v2 live side by side
&lt;/h2&gt;

&lt;p&gt;When a change truly cannot be made additively, you do not flip a switch. You run the old and new shapes &lt;strong&gt;in parallel&lt;/strong&gt;, announce a deprecation, and only then sunset the old one. Here is the shape of that lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnRzPGJyLz5tb2JpbGUgKyBwYXJ0bmVycyIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fZ3coIkFQSSBHYXRld2F5PGJyLz5yb3V0ZXMgYnkgdmVyc2lvbiIpCiAgY2xhc3Mgbl9ndyBlZGdlOwogIG5fdjEoInYxIGhhbmRsZXI8YnIvPm9sZCBzaGFwZSIpCiAgY2xhc3Mgbl92MSBjb21wdXRlOwogIG5fdjIoInYyIGhhbmRsZXI8YnIvPm5ldyBzaGFwZSIpCiAgY2xhc3Mgbl92MiBjb21wdXRlOwogIG5fY29yZSgiRG9tYWluIGxvZ2ljPGJyLz5zaGFyZWQgY29yZSIpCiAgY2xhc3Mgbl9jb3JlIGRhdGE7CiAgbl9tZXRyaWNzKCJVc2FnZSBtZXRyaWNzPGJyLz53aG8gc3RpbGwgY2FsbHMgdjE_IikKICBjbGFzcyBuX21ldHJpY3Mgb2JzZXJ2YWJpbGl0eTsKICBuX2NsaWVudCAtLT58InJlcXVlc3QifCBuX2d3CiAgbl9ndyAtLT58ImRlcHJlY2F0ZWQifCBuX3YxCiAgbl9ndyAtLT58InByZWZlcnJlZCJ8IG5fdjIKICBuX3YxIC0tPiBuX2NvcmUKICBuX3YyIC0tPiBuX2NvcmUKICBuX2d3IC0uLT58InRyYWNrIGNhbGxlcnMifCBuX21ldHJpY3M%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnRzPGJyLz5tb2JpbGUgKyBwYXJ0bmVycyIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fZ3coIkFQSSBHYXRld2F5PGJyLz5yb3V0ZXMgYnkgdmVyc2lvbiIpCiAgY2xhc3Mgbl9ndyBlZGdlOwogIG5fdjEoInYxIGhhbmRsZXI8YnIvPm9sZCBzaGFwZSIpCiAgY2xhc3Mgbl92MSBjb21wdXRlOwogIG5fdjIoInYyIGhhbmRsZXI8YnIvPm5ldyBzaGFwZSIpCiAgY2xhc3Mgbl92MiBjb21wdXRlOwogIG5fY29yZSgiRG9tYWluIGxvZ2ljPGJyLz5zaGFyZWQgY29yZSIpCiAgY2xhc3Mgbl9jb3JlIGRhdGE7CiAgbl9tZXRyaWNzKCJVc2FnZSBtZXRyaWNzPGJyLz53aG8gc3RpbGwgY2FsbHMgdjE_IikKICBjbGFzcyBuX21ldHJpY3Mgb2JzZXJ2YWJpbGl0eTsKICBuX2NsaWVudCAtLT58InJlcXVlc3QifCBuX2d3CiAgbl9ndyAtLT58ImRlcHJlY2F0ZWQifCBuX3YxCiAgbl9ndyAtLT58InByZWZlcnJlZCJ8IG5fdjIKICBuX3YxIC0tPiBuX2NvcmUKICBuX3YyIC0tPiBuX2NvcmUKICBuX2d3IC0uLT58InRyYWNrIGNhbGxlcnMifCBuX21ldHJpY3M%3FbgColor%3D0d1017%26type%3Dpng" alt="v1 and v2 served together, then v1 is deprecated, then sunset, clients migrate in the gap." width="894" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;v1 and v2 served together, then v1 is deprecated, then sunset, clients migrate in the gap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The key insight is the shared &lt;strong&gt;Domain logic&lt;/strong&gt; node. Both versions call the same core; they differ only at the edge, in how they shape the response. A version is a translation layer, not a fork of your business logic. The &lt;strong&gt;Usage metrics&lt;/strong&gt; node is what tells you when it is actually safe to sunset v1, never sunset on a guess.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship v2 alongside v1&lt;/strong&gt;: Add the new shape behind a new version. v1 keeps working untouched. Both handlers call the same domain core so behaviour stays identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Announce the deprecation&lt;/strong&gt;: Mark v1 deprecated in the docs, return a &lt;code&gt;Deprecation&lt;/code&gt; and &lt;code&gt;Sunset&lt;/code&gt; header on v1 responses, and tell known clients directly. Give a real date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure who still calls v1&lt;/strong&gt;: Instrument the v1 handler. You cannot sunset what you cannot see. Watch the call count trend toward zero, and identify the stragglers by API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nudge the stragglers&lt;/strong&gt;: As the date nears, email the remaining callers, raise the log level, optionally inject brief artificial latency or warning bodies. Do not surprise anyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunset v1&lt;/strong&gt;: Once usage is at (or near) zero and the date has passed, return 410 Gone for v1, then delete the handler. The contract has ended on agreed terms.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What is safe vs what breaks
&lt;/h2&gt;

&lt;p&gt;Most of the day-to-day skill is simply knowing which bucket a change falls into. Additive changes ship any time, no version bump. Breaking changes force the lifecycle above. Memorize this table and most of the fear goes away.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Additive / safe&lt;/th&gt;
&lt;th&gt;Breaking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add a new optional field to a response&lt;/td&gt;
&lt;td&gt;Yes, old clients ignore it&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a new endpoint or method&lt;/td&gt;
&lt;td&gt;Yes, nobody called it before&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a new optional request parameter&lt;/td&gt;
&lt;td&gt;Yes, old callers omit it, you default it&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a new value to an enum&lt;/td&gt;
&lt;td&gt;Usually, only if clients tolerate unknowns&lt;/td&gt;
&lt;td&gt;If clients hard-switch on known values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove or rename a response field&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Yes, the rename that broke the phones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make an optional field required&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Yes, old requests now fail validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change a field's type or format&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;"42"&lt;/code&gt; to &lt;code&gt;42&lt;/code&gt; breaks parsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tighten validation / shrink a range&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Yes, previously valid input now rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change default behaviour or sort order&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Yes, silent breakage, the worst kind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The two buckets every API change falls into.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; Enum additions are the classic trap. Adding a new &lt;code&gt;status&lt;/code&gt; value is additive only if every client treats unknown values gracefully. Plenty of clients do &lt;code&gt;switch (status)&lt;/code&gt; with no default and crash on anything new. Document, from day one, that clients must tolerate unknown enum members, then additions stay safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Versioning strategies: where the version lives
&lt;/h2&gt;

&lt;p&gt;When you do need a breaking version, the question is &lt;em&gt;where the version number goes&lt;/em&gt;. There are three mainstream answers, and the honest truth is that the best one is usually &lt;strong&gt;none of them&lt;/strong&gt;, keep changes additive and never bump at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;How it looks&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Additive-only (no version)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/users/42&lt;/code&gt; forever; only ever add fields&lt;/td&gt;
&lt;td&gt;Best default, no migration, no sprawl. Requires discipline: you can never take away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URI versioning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/v1/users/42&lt;/code&gt; then &lt;code&gt;/v2/users/42&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dead obvious, cache-friendly, easy to route. But the URL of a resource changes, which is conceptually wrong, and tends to breed v3, v4, v5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header versioning&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Accept: application/vnd.api.v2+json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL stays stable per resource; clean in theory. But invisible in a browser, harder to cache and debug, easy to forget to set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Three strategies for breaking versions, plus the one you should reach for first.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick additive-only as your default.&lt;/strong&gt; Reach for &lt;strong&gt;URI versioning&lt;/strong&gt; when you genuinely need a clean break and you value how visible and routable it is, it is the most common choice for public APIs precisely because it is impossible to misread. Reach for &lt;strong&gt;header versioning&lt;/strong&gt; only if a stable resource URL matters more to you than ease of debugging, and your clients are disciplined services rather than humans poking at URLs.&lt;/p&gt;

&lt;p&gt;Whatever you pick, &lt;strong&gt;version the whole API, not individual fields.&lt;/strong&gt; Per-field versioning (&lt;code&gt;name_v2&lt;/code&gt;, &lt;code&gt;name_v3&lt;/code&gt;) is how you end up with a response object that is a museum of every decision you ever regretted. One version axis, applied consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: an additive change done right
&lt;/h2&gt;

&lt;p&gt;Here is the rename from the opening story, done the safe way. We never remove &lt;code&gt;user_name&lt;/code&gt;. We add &lt;code&gt;username&lt;/code&gt; next to it, both populated from the same source. Old clients keep reading the old key; new clients adopt the new one. No version bump needed, this is pure addition.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /users/42, response evolves additively&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;BEFORE:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;original&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;contract&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sribalaji"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sri@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;AFTER:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;additive,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;stays,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;added.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Both&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;populated.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Nothing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;removed,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;so&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nothing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;breaks.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sribalaji"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;deprecated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;still&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;served&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;clients&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sribalaji"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preferred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;clients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sri@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sri Balaji"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;brand-new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;optional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;also&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;safe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, the contract is something you assert against, not something you let drift. A contract test pins the shape so a careless refactor fails CI instead of failing production. Here is the idea in TypeScript.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user.contract.test.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET /users/:id contract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keeps every field old clients depend on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// These keys are PROMISES. Removing one must fail here,&lt;/span&gt;
    &lt;span class="c1"&gt;// loudly, in CI, long before it reaches a phone.&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// deprecated, still required&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Types are part of the contract too.&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;may add new fields freely&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Additive fields are allowed, we assert presence, not absence.&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contract tests are the seatbelt for the whole discipline. In a multi-service shop, &lt;strong&gt;consumer-driven contracts&lt;/strong&gt; go further: each consumer publishes the subset of the shape it actually reads, and the provider's CI verifies it never breaks any published expectation. The provider learns it broke a client &lt;em&gt;before&lt;/em&gt; deploying, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Renaming or removing a field in place.&lt;/strong&gt; The original sin from the opening story. If the name is wrong, add the right one beside it and deprecate the old one. Never delete a field someone might read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No deprecation window.&lt;/strong&gt; Marking something deprecated and removing it the same week is not a deprecation, it is an ambush. Announce a date, return &lt;code&gt;Deprecation&lt;/code&gt; and &lt;code&gt;Sunset&lt;/code&gt; headers, and measure usage until it is genuinely zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunsetting on a guess.&lt;/strong&gt; Removing v1 because "surely nobody uses it anymore" without metrics. Instrument the old path first. The one straggler is always your biggest partner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version sprawl.&lt;/strong&gt; Bumping the major version for a change that could have been additive. Every live version is a maintenance tax forever. Most changes should add a field, not a &lt;code&gt;/v3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-field or per-endpoint versions.&lt;/strong&gt; &lt;code&gt;name_v2&lt;/code&gt;, &lt;code&gt;/v3/users&lt;/code&gt; next to &lt;code&gt;/v1/orders&lt;/code&gt;, now nobody knows what "the API version" even means. One version axis, applied to the whole surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tightening silently.&lt;/strong&gt; Shrinking a max length, adding a required field, or changing a default sort. These pass your tests and break clients quietly. Treat any narrowing of accepted input as breaking.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can add to an API; you cannot take away. The moment a client reads your shape, the shape is a promise.&lt;/li&gt;
&lt;li&gt;Additive changes, new optional fields, new endpoints, new tolerated enum values, ship any time, no version bump.&lt;/li&gt;
&lt;li&gt;Breaking changes, renames, removals, type changes, new required fields, tighter validation, force the full lifecycle.&lt;/li&gt;
&lt;li&gt;The lifecycle is: serve v2 beside v1 → announce deprecation with a date → measure real usage → nudge stragglers → sunset.&lt;/li&gt;
&lt;li&gt;Default to additive-only (no version). Reach for URI versioning for visibility, header versioning only for stable URLs.&lt;/li&gt;
&lt;li&gt;Version the whole API on one axis. Per-field versions and v3/v4/v5 sprawl are how you drown in maintenance.&lt;/li&gt;
&lt;li&gt;Contract tests (and consumer-driven contracts) catch a broken promise in CI instead of in the App Store review queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Evolution is one pillar of API design. Pair it with the fundamentals of resource modelling and the security model so the contract you are so carefully preserving is also a good contract to begin with.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;a href="https://thesimplifiedtech.com/blog/rest-api-design" rel="noopener noreferrer"&gt;REST API Design&lt;/a&gt;, get the resource shapes right before you have to evolve them.&lt;/li&gt;
&lt;li&gt;Then read &lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;Secure API Design&lt;/a&gt;, versioning a leaky contract just preserves the leak.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/backend-engineer" rel="noopener noreferrer"&gt;Backend Engineer path&lt;/a&gt; to see where API evolution sits among data modelling, messaging, and reliability.&lt;/li&gt;
&lt;li&gt;Practice routing and gateways in the &lt;a href="https://thesimplifiedtech.com/labs/networking" rel="noopener noreferrer"&gt;networking lab&lt;/a&gt;, the gateway is where version routing actually lives.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/api-design-evolution-and-versioning" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Database Transactions &amp; Consistency</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Mon, 05 Oct 2026 19:32:44 +0000</pubDate>
      <link>https://dev.to/sri2614/database-transactions-consistency-e73</link>
      <guid>https://dev.to/sri2614/database-transactions-consistency-e73</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Transactions guarantee all-or-nothing, but the lever that actually prevents anomalies is your &lt;strong&gt;isolation level&lt;/strong&gt;. Learn to pick it deliberately, choose optimistic vs pessimistic locking by contention, and stop treating eventual consistency as a bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two transfers, one disappearing $100&lt;/li&gt;
&lt;li&gt;What a transaction promises&lt;/li&gt;
&lt;li&gt;The shape of a transaction&lt;/li&gt;
&lt;li&gt;Isolation levels and the anomalies they prevent&lt;/li&gt;
&lt;li&gt;Doing it in SQL: commit, rollback, and locking&lt;/li&gt;
&lt;li&gt;Optimistic vs pessimistic, and a word on eventual consistency&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two transfers, one disappearing $100
&lt;/h2&gt;

&lt;p&gt;Alice has &lt;strong&gt;$100&lt;/strong&gt;. At 12:00:00.000 two transfer requests hit your server at the same time: one moves $100 to Bob, the other moves $100 to Carol. Each request runs the same code: read Alice's balance ($100), check it's enough (it is), subtract $100, write the new balance ($0). Both reads happen before either write lands. Both checks pass. Both writes succeed. Bob gets $100, Carol gets $100, and Alice's balance is $0, not -$100. You just &lt;strong&gt;created $100 out of thin air&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nobody wrote a bug. The logic is correct for one request at a time. It falls apart because two requests interleaved, and nothing forced them to take turns. This is the entire reason database transactions exist: to make a sequence of steps behave as if it were a single, indivisible, isolated operation, even when thousands of them run concurrently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Backend engineers who use &lt;code&gt;BEGIN&lt;/code&gt;/&lt;code&gt;COMMIT&lt;/code&gt; but have never been bitten hard enough to learn &lt;em&gt;why&lt;/em&gt; they matter. You'll leave understanding ACID concretely, knowing which isolation level prevents which anomaly, when to reach for optimistic vs pessimistic locking, and what "eventual consistency" actually buys and costs. No CS degree assumed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a transaction promises
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A transaction is a group of operations that the database treats as a single unit: all of them happen, or none of them do, and to everyone else it looks instantaneous.&lt;/p&gt;

&lt;p&gt;The one-sentence definition of ACID&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The classic example is a bank transfer. Moving $100 from Alice to Bob is really &lt;strong&gt;two&lt;/strong&gt; writes: subtract $100 from Alice, add $100 to Bob. If the server crashes between them, you must never end up in a world where Alice was debited but Bob was never credited. The money has to &lt;em&gt;fully&lt;/em&gt; move or &lt;em&gt;not move at all&lt;/em&gt;. A transaction is the contract that guarantees exactly that.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The transfer either fully completes or is fully torn up, never half-done.&lt;/td&gt;
&lt;td&gt;Atomicity, all writes commit, or all roll back.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Money is never created or destroyed; the books always balance afterward.&lt;/td&gt;
&lt;td&gt;Consistency, every committed state satisfies your constraints and invariants.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;While you're mid-transfer, nobody else sees Alice debited but Bob not yet credited.&lt;/td&gt;
&lt;td&gt;Isolation, concurrent transactions don't see each other's partial work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Once the receipt prints, the money stays moved even if the power dies a second later.&lt;/td&gt;
&lt;td&gt;Durability, a committed transaction survives crashes and restarts.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;ACID, mapped to a single bank transfer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those four letters are &lt;strong&gt;ACID&lt;/strong&gt;. Atomicity and durability are mostly the database's job, it writes to a durable log before it acknowledges your &lt;code&gt;COMMIT&lt;/code&gt;. Consistency is shared: the database enforces the constraints &lt;em&gt;you&lt;/em&gt; declare (foreign keys, &lt;code&gt;CHECK&lt;/code&gt;, &lt;code&gt;UNIQUE&lt;/code&gt;), but it can't know your business rules unless you encode them. &lt;strong&gt;Isolation&lt;/strong&gt; is the subtle one, and where most real bugs live, it's where the $100-from-thin-air problem hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a transaction
&lt;/h2&gt;

&lt;p&gt;Every transaction has the same skeleton: you &lt;code&gt;BEGIN&lt;/code&gt;, run one or more operations, then either &lt;code&gt;COMMIT&lt;/code&gt; (make it permanent and visible) or &lt;code&gt;ROLLBACK&lt;/code&gt; (throw it all away as if it never happened). Below, two transactions run against the same rows at once, and isolation is what decides what each one is allowed to see of the other.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fYmVnaW4oIkJFR0lOPGJyLz5UeG4gQSBzdGFydHMiKQogIGNsYXNzIG5fYmVnaW4gY2xpZW50OwogIG5fb3BzKCJPcGVyYXRpb25zPGJyLz5yZWFkcyArIHdyaXRlcyIpCiAgY2xhc3Mgbl9vcHMgY29tcHV0ZTsKICBuX2RlY2lkZSgiQ09NTUlUPzxici8-c3VjY2VzcyBwYXRoIikKICBjbGFzcyBuX2RlY2lkZSBjb21wdXRlOwogIG5fZGIoIkR1cmFibGUgc3RvcmU8YnIvPldBTCArIHRhYmxlcyIpCiAgY2xhc3Mgbl9kYiBkYXRhOwogIG5fcm9sbGJhY2soIlJPTExCQUNLPGJyLz5lcnJvciBwYXRoLCB1bmRvIGFsbCIpCiAgY2xhc3Mgbl9yb2xsYmFjayBleHRlcm5hbDsKICBuX2JlZ2luQigiQkVHSU48YnIvPlR4biBCIChjb25jdXJyZW50KSIpCiAgY2xhc3Mgbl9iZWdpbkIgY2xpZW50OwogIG5fb3BzQigiT3BlcmF0aW9uczxici8-c2FtZSByb3dzIikKICBjbGFzcyBuX29wc0IgY29tcHV0ZTsKICBuX2JlZ2luIC0tPiBuX29wcwogIG5fb3BzIC0tPiBuX2RlY2lkZQogIG5fZGVjaWRlIC0tPnwiY29tbWl0Inwgbl9kYgogIG5fZGVjaWRlIC0uLT58Im9uIGVycm9yInwgbl9yb2xsYmFjawogIG5fcm9sbGJhY2sgLS4tPnwidW5kbyJ8IG5fb3BzCiAgbl9iZWdpbkIgLS0-IG5fb3BzQgogIG5fb3BzQiAtLi0-fCJjb250ZW5kcyJ8IG5fZGI%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fYmVnaW4oIkJFR0lOPGJyLz5UeG4gQSBzdGFydHMiKQogIGNsYXNzIG5fYmVnaW4gY2xpZW50OwogIG5fb3BzKCJPcGVyYXRpb25zPGJyLz5yZWFkcyArIHdyaXRlcyIpCiAgY2xhc3Mgbl9vcHMgY29tcHV0ZTsKICBuX2RlY2lkZSgiQ09NTUlUPzxici8-c3VjY2VzcyBwYXRoIikKICBjbGFzcyBuX2RlY2lkZSBjb21wdXRlOwogIG5fZGIoIkR1cmFibGUgc3RvcmU8YnIvPldBTCArIHRhYmxlcyIpCiAgY2xhc3Mgbl9kYiBkYXRhOwogIG5fcm9sbGJhY2soIlJPTExCQUNLPGJyLz5lcnJvciBwYXRoLCB1bmRvIGFsbCIpCiAgY2xhc3Mgbl9yb2xsYmFjayBleHRlcm5hbDsKICBuX2JlZ2luQigiQkVHSU48YnIvPlR4biBCIChjb25jdXJyZW50KSIpCiAgY2xhc3Mgbl9iZWdpbkIgY2xpZW50OwogIG5fb3BzQigiT3BlcmF0aW9uczxici8-c2FtZSByb3dzIikKICBjbGFzcyBuX29wc0IgY29tcHV0ZTsKICBuX2JlZ2luIC0tPiBuX29wcwogIG5fb3BzIC0tPiBuX2RlY2lkZQogIG5fZGVjaWRlIC0tPnwiY29tbWl0Inwgbl9kYgogIG5fZGVjaWRlIC0uLT58Im9uIGVycm9yInwgbl9yb2xsYmFjawogIG5fcm9sbGJhY2sgLS4tPnwidW5kbyJ8IG5fb3BzCiAgbl9iZWdpbkIgLS0-IG5fb3BzQgogIG5fb3BzQiAtLi0-fCJjb250ZW5kcyJ8IG5fZGI%3FbgColor%3D0d1017%26type%3Dpng" alt="One transaction's lifecycle (top row) and a second concurrent transaction (bottom row) contending for the same rows; iso" width="809" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One transaction's lifecycle (top row) and a second concurrent transaction (bottom row) contending for the same rows; isolation governs what each sees of the other's uncommitted work.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;BEGIN&lt;/strong&gt;: You open a transaction. The database takes a consistent snapshot point and starts tracking every change you make so it can undo them if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run operations&lt;/strong&gt;: Your reads and writes happen, but to other sessions they're invisible (or visible-but-blocked, depending on isolation level). Nothing is permanent yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide: COMMIT or ROLLBACK&lt;/strong&gt;: If every step succeeded, COMMIT durably writes the changes and makes them visible to everyone. If anything failed, ROLLBACK discards every change, the database returns to exactly its pre-BEGIN state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency is resolved here&lt;/strong&gt;: When Txn A and Txn B touch the same rows, the isolation level decides whether B sees A's uncommitted data, blocks until A finishes, or fails with a conflict it must retry.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Isolation levels and the anomalies they prevent
&lt;/h2&gt;

&lt;p&gt;Perfect isolation is expensive, making every transaction take strict turns kills throughput. So SQL defines a ladder of &lt;strong&gt;isolation levels&lt;/strong&gt; that trade safety for concurrency. Each rung permits certain &lt;em&gt;anomalies&lt;/em&gt;: weird results caused by interleaving. Know the three you'll meet most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dirty read&lt;/strong&gt;, you read a row another transaction has changed but not yet committed. If it rolls back, you acted on data that never officially existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-repeatable read&lt;/strong&gt;, you read a row twice in one transaction and get different values, because someone else committed an &lt;code&gt;UPDATE&lt;/code&gt; in between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phantom read&lt;/strong&gt;, you run the same &lt;code&gt;WHERE&lt;/code&gt; query twice and get a different &lt;em&gt;set of rows&lt;/em&gt;, because someone else committed an &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; matching your filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Isolation level&lt;/th&gt;
&lt;th&gt;Dirty read&lt;/th&gt;
&lt;th&gt;Non-repeatable read&lt;/th&gt;
&lt;th&gt;Phantom read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read uncommitted&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read committed&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable read&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Allowed*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The four standard SQL isolation levels and which anomalies each still allows. Lower in the table = safer but more contention.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Defaults differ, check yours:&lt;/strong&gt; PostgreSQL defaults to &lt;strong&gt;Read committed&lt;/strong&gt;; its &lt;strong&gt;Repeatable read&lt;/strong&gt; is snapshot-based and actually prevents phantoms too (the &lt;code&gt;*&lt;/code&gt; above), and its &lt;strong&gt;Serializable&lt;/strong&gt; detects conflicts and aborts one transaction to retry. MySQL/InnoDB defaults to &lt;strong&gt;Repeatable read&lt;/strong&gt;. Never assume, &lt;code&gt;SHOW transaction_isolation;&lt;/code&gt; and read your engine's docs, because the same level name can mean subtly different things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The practical rule: &lt;strong&gt;Read committed&lt;/strong&gt; is a sane default for most CRUD apps. Bump to &lt;strong&gt;Repeatable read&lt;/strong&gt; or &lt;strong&gt;Serializable&lt;/strong&gt; only for transactions where correctness depends on a stable view, like a report that sums rows it also reads back, or a balance check that must hold for the whole transaction. Higher levels mean more blocking and more retryable conflicts, so pay for them only where you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it in SQL: commit, rollback, and locking
&lt;/h2&gt;

&lt;p&gt;Here's the bank transfer done correctly, wrapped in a transaction so it's all-or-nothing, with an explicit &lt;code&gt;ROLLBACK&lt;/code&gt; path. The &lt;code&gt;CHECK&lt;/code&gt; keeps the books consistent even if logic slips.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;transfer.sql&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Debit Alice; the CHECK constraint refuses to let her go negative&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- If no row matched (insufficient funds), bail out and undo everything&lt;/span&gt;
&lt;span class="c1"&gt;-- (application checks the affected row count; on 0 it issues:)&lt;/span&gt;
&lt;span class="c1"&gt;--   ROLLBACK;&lt;/span&gt;

&lt;span class="c1"&gt;-- Credit Bob&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'bob'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- both writes land together, or neither does&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But two concurrent transfers can still race, the lost-update problem from the hook. There are two ways to stop it. &lt;strong&gt;Pessimistic locking&lt;/strong&gt; grabs the row up front so nobody else can touch it until you're done:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pessimistic.sql&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Lock Alice's row: any other txn that also does SELECT ... FOR UPDATE&lt;/span&gt;
&lt;span class="c1"&gt;-- (or tries to UPDATE it) blocks here until this transaction ends.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Now we hold the lock; the read-check-write is safe.&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'bob'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- lock releases here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Optimistic locking&lt;/strong&gt; assumes conflicts are rare. You don't lock, you add a &lt;code&gt;version&lt;/code&gt; column, read it, and only write if it hasn't changed since you read it. If someone else got there first, your &lt;code&gt;UPDATE&lt;/code&gt; affects zero rows and you retry:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;optimistic.sql&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: read the row AND its current version (no lock held)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;--   -&amp;gt; balance = 100, version = 7&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: write only if the version is still 7. Note version bumps.&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: check the affected row count.&lt;/span&gt;
&lt;span class="c1"&gt;--   1 row  -&amp;gt; success, you won the race.&lt;/span&gt;
&lt;span class="c1"&gt;--   0 rows -&amp;gt; someone else updated first (version is now 8);&lt;/span&gt;
&lt;span class="c1"&gt;--             re-read and retry the whole operation.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Optimistic vs pessimistic, and a word on eventual consistency
&lt;/h2&gt;

&lt;p&gt;Neither lock is "better", they're bets on how often conflicts happen. &lt;strong&gt;Pessimistic&lt;/strong&gt; locking prevents conflicts by blocking; it's right when contention is high or the work between read and write is short, but it costs throughput and risks deadlocks if you lock rows in inconsistent orders. &lt;strong&gt;Optimistic&lt;/strong&gt; locking lets everyone proceed and only pays a cost when a conflict actually occurs; it shines under low contention and for long think-time (e.g. a user editing a form for two minutes), but you must handle the retry path or you'll silently drop the loser's write.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pessimistic&lt;/th&gt;
&lt;th&gt;Optimistic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mechanism&lt;/td&gt;
&lt;td&gt;Lock the row (FOR UPDATE)&lt;/td&gt;
&lt;td&gt;Version/timestamp check on write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best when&lt;/td&gt;
&lt;td&gt;High contention, short critical section&lt;/td&gt;
&lt;td&gt;Low contention, long think-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Blocking, deadlock risk&lt;/td&gt;
&lt;td&gt;Retries when conflicts occur&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;Waits / timeouts&lt;/td&gt;
&lt;td&gt;Write affects 0 rows, must retry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Choosing a concurrency strategy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything so far assumes one database that can give you a single, instantly-consistent answer. Spread data across many machines and you hit the &lt;strong&gt;CAP&lt;/strong&gt; trade-off: when the network between nodes breaks (a Partition), you can only keep one of &lt;strong&gt;Consistency&lt;/strong&gt; (every read sees the latest write) or &lt;strong&gt;Availability&lt;/strong&gt; (every request still gets an answer). You can't have both during a partition, and partitions are not optional, so the real choice is C-vs-A under failure.&lt;/p&gt;

&lt;p&gt;Many distributed stores pick availability and offer &lt;strong&gt;eventual consistency&lt;/strong&gt;: a write may not be visible everywhere immediately, but if writes stop, all replicas &lt;em&gt;converge&lt;/em&gt; to the same value given enough time. That's why you sometimes post a comment and don't see it on a refresh, your read hit a replica that hadn't caught up. It's a perfectly good trade for likes, feeds, and caches; it's a terrible trade for account balances, which is exactly where you keep a strongly-consistent, ACID-transactional database. Pick per-use-case, not per-religion. (More on the storage side in &lt;a href="https://thesimplifiedtech.com/blog/sql-vs-nosql" rel="noopener noreferrer"&gt;SQL vs NoSQL&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No transaction around multi-step writes.&lt;/strong&gt; Any operation that's two or more writes (transfer, order + line items, create-user + create-profile) must be one transaction. Without it, a crash or error leaves you half-written and corrupt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong isolation level.&lt;/strong&gt; Running a balance check or a sum-and-reconcile at Read committed lets values shift mid-transaction. Match the level to the invariant you need to hold; default Read committed is not enough for everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-running transactions.&lt;/strong&gt; Holding a transaction open across a network call, an external API, or user think-time keeps locks and snapshots alive, bloating the database and blocking others. Keep transactions short; do slow work &lt;em&gt;outside&lt;/em&gt; them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the lost update.&lt;/strong&gt; Read-modify-write without &lt;code&gt;FOR UPDATE&lt;/code&gt; or a &lt;code&gt;version&lt;/code&gt; check is the $100-from-thin-air bug. Pick pessimistic or optimistic locking, "neither" is a silent data-loss bug waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catching errors but not rolling back.&lt;/strong&gt; If you swallow an exception mid-transaction and keep going, you may &lt;code&gt;COMMIT&lt;/code&gt; partial work. Always &lt;code&gt;ROLLBACK&lt;/code&gt; on any failure inside the transaction.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A transaction makes many operations behave as one: all commit, or all roll back.&lt;/li&gt;
&lt;li&gt;ACID = Atomicity, Consistency, Isolation, Durability, isolation is where real bugs hide.&lt;/li&gt;
&lt;li&gt;Anomalies climb a ladder: dirty read → non-repeatable read → phantom read.&lt;/li&gt;
&lt;li&gt;Isolation levels trade safety for concurrency; Read committed is a sane default, raise it only where an invariant demands a stable view.&lt;/li&gt;
&lt;li&gt;Stop lost updates with pessimistic locking (&lt;code&gt;FOR UPDATE&lt;/code&gt;) or optimistic locking (a &lt;code&gt;version&lt;/code&gt; column + retry).&lt;/li&gt;
&lt;li&gt;Across machines, CAP forces a Consistency-vs-Availability choice during partitions.&lt;/li&gt;
&lt;li&gt;Eventual consistency converges over time, great for feeds, wrong for balances.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have the vocabulary to reason about correctness under concurrency, the foundation under every backend that touches money, inventory, or counts. Build on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decide &lt;em&gt;where&lt;/em&gt; your transactional data lives, read &lt;a href="https://thesimplifiedtech.com/blog/sql-vs-nosql" rel="noopener noreferrer"&gt;SQL vs NoSQL&lt;/a&gt; to weigh strong consistency against scale.&lt;/li&gt;
&lt;li&gt;Move slow work out of transactions and off the request path with &lt;a href="https://thesimplifiedtech.com/blog/async-processing-queues-and-workers" rel="noopener noreferrer"&gt;Async Processing: Queues &amp;amp; Workers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;See how these fit a full role on the &lt;a href="https://thesimplifiedtech.com/career-paths/backend-engineer" rel="noopener noreferrer"&gt;Backend Engineer path&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/database-transactions-and-consistency" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Async Processing: Queues &amp; Workers</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sun, 04 Oct 2026 15:35:33 +0000</pubDate>
      <link>https://dev.to/sri2614/async-processing-queues-workers-4df3</link>
      <guid>https://dev.to/sri2614/async-processing-queues-workers-4df3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Move slow work off the request path with queues and workers so APIs stay fast. The catch: at-least-once delivery means you must make workers &lt;strong&gt;idempotent&lt;/strong&gt; and add retries plus a dead-letter queue, or duplicates and lost jobs will bite.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The checkout that times out&lt;/li&gt;
&lt;li&gt;The principle: acknowledge fast, do the slow work later&lt;/li&gt;
&lt;li&gt;The picture: API to queue to worker pool&lt;/li&gt;
&lt;li&gt;Sync vs async: what you are actually trading&lt;/li&gt;
&lt;li&gt;Enqueue a job, write an idempotent worker&lt;/li&gt;
&lt;li&gt;Idempotency, retries, and the dead-letter queue&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checkout that times out
&lt;/h2&gt;

&lt;p&gt;A customer clicks &lt;strong&gt;Place order&lt;/strong&gt;. Your handler charges the card, writes the order row, then, still inside the same HTTP request, renders a receipt, calls the email provider, pings the warehouse webhook, and updates the analytics warehouse. The card charge took 200ms. Everything after it took 4 seconds. The mobile client gave up at 3, showed a spinning error, and the customer tapped &lt;strong&gt;Place order&lt;/strong&gt; again.&lt;/p&gt;

&lt;p&gt;Now you have two charges, one angry customer, and a support ticket. Nothing was broken, the code did exactly what you wrote. The mistake was &lt;em&gt;where&lt;/em&gt; you ran the slow work: inline, in the request path, holding the user hostage while you talked to three external systems that have nothing to do with whether the order succeeded.&lt;/p&gt;

&lt;p&gt;The order was &lt;em&gt;done&lt;/em&gt; the moment the charge cleared and the row was written. The email, the webhook, the analytics, those are follow-up work. They should not be able to slow down, or fail, the thing the user is actually waiting for. This is the problem async processing solves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Backend engineers who can build a CRUD API and now need to make it fast and resilient under real load. You should be comfortable with HTTP request/response and a database. You do not need prior queue experience, we build the mental model from zero. If you have shipped a feature that "sometimes sends two emails," this article is for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The principle: acknowledge fast, do the slow work later
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Do the minimum work needed to keep your promise to the user, acknowledge immediately, and push everything else onto a queue for a worker to finish in the background.&lt;/p&gt;

&lt;p&gt;The async processing rule&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The request path should only contain work that is &lt;strong&gt;required for correctness and that the user is waiting on the result of&lt;/strong&gt;. Charging the card qualifies. Sending a confirmation email does not, the user does not stare at the screen until the email lands. So the handler does the essential bit, drops a message describing the rest onto a queue, and returns &lt;code&gt;202 Accepted&lt;/code&gt; in milliseconds. A separate process picks the message up later and does the slow part.&lt;/p&gt;

&lt;p&gt;This splits one fragile synchronous chain into two independent halves. The &lt;strong&gt;producer&lt;/strong&gt; (your API) only needs the queue to be up. The &lt;strong&gt;consumer&lt;/strong&gt; (your worker) can be slow, can restart, can fail and retry, and the user never sees any of it. You have traded a tiny bit of immediacy (the email arrives in 2 seconds, not 0) for a large amount of latency and reliability.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Waiter clips the order ticket to the rail and walks away&lt;/td&gt;
&lt;td&gt;Producer enqueues a job and returns immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The ticket rail holding orders in line&lt;/td&gt;
&lt;td&gt;The message queue buffering jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cooks pull the next ticket when they are free&lt;/td&gt;
&lt;td&gt;Worker pool pulling jobs at its own pace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A rush doesn't stop waiters taking orders, tickets just pile up&lt;/td&gt;
&lt;td&gt;Load spike doesn't slow the API; the queue absorbs the backlog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A dropped dish gets re-fired from the same ticket&lt;/td&gt;
&lt;td&gt;A failed job is retried from the same message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;A busy restaurant kitchen is a producer/consumer system you have already seen work.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture: API to queue to worker pool
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPk1vYmlsZSAvIHdlYiIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fYXBpKCJBUEk8YnIvPlByb2R1Y2VyIikKICBjbGFzcyBuX2FwaSBjb21wdXRlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5PcmRlciByb3cgKyBjaGFyZ2UiKQogIGNsYXNzIG5fZGIgZGF0YTsKICBuX3F1ZXVlKCJNZXNzYWdlIFF1ZXVlPGJyLz5TUVMgLyBSYWJiaXRNUSIpCiAgY2xhc3Mgbl9xdWV1ZSBxdWV1ZTsKICBuX3dvcmtlcnMoIldvcmtlciBQb29sPGJyLz5Db25zdW1lcnMiKQogIGNsYXNzIG5fd29ya2VycyBjb21wdXRlOwogIG5fZW1haWwoIkVtYWlsIFByb3ZpZGVyPGJyLz5TaWRlIGVmZmVjdCIpCiAgY2xhc3Mgbl9lbWFpbCBleHRlcm5hbDsKICBuX2RscSgiRGVhZC1MZXR0ZXIgUXVldWU8YnIvPlBvaXNvbiBtZXNzYWdlcyIpCiAgY2xhc3Mgbl9kbHEgcXVldWU7CiAgbl9jbGllbnQgLS0-fCJQT1NUIC9vcmRlcnMifCBuX2FwaQogIG5fYXBpIC0tPnwiY2hhcmdlICsgd3JpdGUifCBuX2RiCiAgbl9hcGkgLS0-fCJlbnF1ZXVlIGpvYiJ8IG5fcXVldWUKICBuX3F1ZXVlIC0tPnwiZGVsaXZlciJ8IG5fd29ya2VycwogIG5fd29ya2VycyAtLi0-fCJzZW5kInwgbl9lbWFpbAogIG5fd29ya2VycyAtLi0-fCJtYXggcmV0cmllcyBleGNlZWRlZCJ8IG5fZGxx%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPk1vYmlsZSAvIHdlYiIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fYXBpKCJBUEk8YnIvPlByb2R1Y2VyIikKICBjbGFzcyBuX2FwaSBjb21wdXRlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5PcmRlciByb3cgKyBjaGFyZ2UiKQogIGNsYXNzIG5fZGIgZGF0YTsKICBuX3F1ZXVlKCJNZXNzYWdlIFF1ZXVlPGJyLz5TUVMgLyBSYWJiaXRNUSIpCiAgY2xhc3Mgbl9xdWV1ZSBxdWV1ZTsKICBuX3dvcmtlcnMoIldvcmtlciBQb29sPGJyLz5Db25zdW1lcnMiKQogIGNsYXNzIG5fd29ya2VycyBjb21wdXRlOwogIG5fZW1haWwoIkVtYWlsIFByb3ZpZGVyPGJyLz5TaWRlIGVmZmVjdCIpCiAgY2xhc3Mgbl9lbWFpbCBleHRlcm5hbDsKICBuX2RscSgiRGVhZC1MZXR0ZXIgUXVldWU8YnIvPlBvaXNvbiBtZXNzYWdlcyIpCiAgY2xhc3Mgbl9kbHEgcXVldWU7CiAgbl9jbGllbnQgLS0-fCJQT1NUIC9vcmRlcnMifCBuX2FwaQogIG5fYXBpIC0tPnwiY2hhcmdlICsgd3JpdGUifCBuX2RiCiAgbl9hcGkgLS0-fCJlbnF1ZXVlIGpvYiJ8IG5fcXVldWUKICBuX3F1ZXVlIC0tPnwiZGVsaXZlciJ8IG5fd29ya2VycwogIG5fd29ya2VycyAtLi0-fCJzZW5kInwgbl9lbWFpbAogIG5fd29ya2VycyAtLi0-fCJtYXggcmV0cmllcyBleGNlZWRlZCJ8IG5fZGxx%3FbgColor%3D0d1017%26type%3Dpng" alt="The API enqueues a job and returns instantly. A pool of workers pulls jobs from the queue and processes them. Jobs that " width="1268" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The API enqueues a job and returns instantly. A pool of workers pulls jobs from the queue and processes them. Jobs that keep failing branch off to a dead-letter queue for humans to inspect.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client calls the API&lt;/strong&gt;: POST /orders. The user is now waiting on this single request, so it must be fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API does the essential work&lt;/strong&gt;: Charge the card, write the order row in the database. This is the part the user actually cares about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API enqueues a job&lt;/strong&gt;: It writes a small message, { type: "send_receipt", orderId: 8842 }, onto the queue. This is a sub-millisecond local call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API returns 202 immediately&lt;/strong&gt;: The user sees a confirmation in under 300ms. The slow follow-up work hasn't happened yet, and that's fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A worker pulls the job&lt;/strong&gt;: Whenever a worker in the pool is free, it grabs the next message and processes it, sends the email, calls the webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker acknowledges success&lt;/strong&gt;: On success it tells the queue to delete the message. On repeated failure, the message is routed to the dead-letter queue for a human.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sync vs async: what you are actually trading
&lt;/h2&gt;

&lt;p&gt;Async is not free, and it is not always right. A read that returns data the user needs &lt;em&gt;now&lt;/em&gt; must stay synchronous. The trade is concrete: you spend complexity to buy latency and reliability. Know what you are buying.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Synchronous (inline)&lt;/th&gt;
&lt;th&gt;Asynchronous (queue + worker)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User-perceived latency&lt;/td&gt;
&lt;td&gt;Sum of every step, as slow as the slowest dependency&lt;/td&gt;
&lt;td&gt;Only the essential work; follow-up runs later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;One downstream failure fails the whole request&lt;/td&gt;
&lt;td&gt;Downstream failure is isolated, retried, and invisible to the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load spikes&lt;/td&gt;
&lt;td&gt;Each request holds resources until done; pile-ups cascade&lt;/td&gt;
&lt;td&gt;Queue absorbs the burst; workers drain it at a steady rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Low, one process, easy to reason about&lt;/td&gt;
&lt;td&gt;Higher, a broker, workers, retries, idempotency, monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure visibility&lt;/td&gt;
&lt;td&gt;Immediate, user sees the error&lt;/td&gt;
&lt;td&gt;Deferred, you need dashboards and a DLQ to see failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Reads the user is waiting on; work required for the response&lt;/td&gt;
&lt;td&gt;Emails, webhooks, thumbnails, exports, anything fire-and-forget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Doing slow work inline vs. handing it to a queue and worker.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enqueue a job, write an idempotent worker
&lt;/h2&gt;

&lt;p&gt;Here is the producer side. The API charges the card, writes the order, then enqueues a tiny message. Notice the message carries an &lt;strong&gt;idempotency key&lt;/strong&gt;, we will use it to make the worker safe to run more than once.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;api/orders.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;place_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Essential work the user is waiting on.
&lt;/span&gt;    &lt;span class="n"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;charge_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Enqueue the slow follow-up work. Sub-millisecond local call.
&lt;/span&gt;    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_receipt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# A stable key so retries don't duplicate side effects.
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receipt:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Return immediately. The email hasn't been sent yet, that's fine.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the consumer. The queue guarantees the message is delivered &lt;strong&gt;at least once&lt;/strong&gt;, which means it may be delivered &lt;em&gt;twice&lt;/em&gt;. The worker must produce the same result whether it runs once or five times. We enforce that by recording the idempotency key before doing the side effect, inside a unique constraint.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;worker/handlers.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_send_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Claim the work atomically. If this key was already processed,
&lt;/span&gt;    &lt;span class="c1"&gt;# the unique index raises and we skip the side effect entirely.
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processed_jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# UNIQUE(key)
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;UniqueViolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# already done, safe to drop the duplicate
&lt;/span&gt;
    &lt;span class="c1"&gt;# Only now do the non-repeatable side effect.
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_receipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# tell the queue to delete the message
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Ack means "done", not "received":&lt;/strong&gt; A worker should acknowledge a message only after the work has fully succeeded. If the worker crashes before acking, the queue redelivers the message to another worker, that is exactly how at-least-once delivery recovers from crashes. Ack too early and a crash silently loses the job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Idempotency, retries, and the dead-letter queue
&lt;/h2&gt;

&lt;h3&gt;
  
  
  At-least-once delivery is why idempotency is mandatory
&lt;/h3&gt;

&lt;p&gt;Almost every production queue (SQS, RabbitMQ, Kafka) delivers &lt;strong&gt;at least once&lt;/strong&gt;, not exactly once. "Exactly once" across a network and a crash is famously close to impossible. So you must assume any message can arrive more than once: a worker processed it, sent the email, then crashed before acking, the queue redelivers, and a second worker sends the email &lt;em&gt;again&lt;/em&gt;. &lt;strong&gt;Idempotency&lt;/strong&gt; is the property that processing the same message twice has the same effect as processing it once. The unique-key check above is how we get it: the first run claims the key, every duplicate hits the constraint and short-circuits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries with backoff
&lt;/h3&gt;

&lt;p&gt;When a job fails, the email provider is down, the network blipped, you do not drop it, you retry. But retry &lt;em&gt;with exponential backoff and jitter&lt;/em&gt;: wait 1s, then 2s, then 4s, then 8s, with a little randomness so a thousand failed jobs don't all retry in lockstep and hammer the recovering dependency. Crucially, retries must be &lt;strong&gt;bounded&lt;/strong&gt;. An infinite retry loop on a permanently-bad message (a "poison message") will spin forever, burn CPU, and block the queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dead-letter queue catches what can't succeed
&lt;/h3&gt;

&lt;p&gt;After N failed attempts (5 is a common default), the broker moves the message to a &lt;strong&gt;dead-letter queue&lt;/strong&gt;, a separate queue for messages that could not be processed. The DLQ is your safety net: nothing is silently lost, the bad message stops blocking the main queue, and a human (or an alert) can inspect it, fix the root cause, and replay it. A queue without a DLQ either loses messages or jams forever. Always wire one up, and always alert on DLQ depth greater than zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Non-idempotent handlers.&lt;/strong&gt; Assuming each message arrives exactly once. Under at-least-once delivery, a duplicate means a double charge or a double email. Make every handler safe to re-run before you ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dead-letter queue.&lt;/strong&gt; Without a DLQ, a poison message either gets lost on the last failed attempt or retries forever and blocks everything behind it. Configure a DLQ and alert when it is non-empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite retries.&lt;/strong&gt; Retrying a permanently-bad message forever wastes resources and starves healthy jobs. Bound retries, use exponential backoff with jitter, then dead-letter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming message ordering.&lt;/strong&gt; Most queues do not guarantee order across messages, and a worker pool processes them in parallel anyway. If "updated" can be processed before "created", your data corrupts. Don't depend on order, or use an ordered/FIFO queue with a partition key and accept the throughput cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acking before the work is done.&lt;/strong&gt; Acknowledging on receipt instead of on success turns every worker crash into a silently dropped job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fat messages.&lt;/strong&gt; Putting a 5MB payload on the queue instead of an ID the worker re-fetches. Keep messages small; pass references, not data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the request path to work the user is waiting on; acknowledge fast and push the rest onto a queue.&lt;/li&gt;
&lt;li&gt;Producer enqueues and returns 202; a worker pool consumes and does the slow side effects later.&lt;/li&gt;
&lt;li&gt;Queues deliver at-least-once, so every message can arrive twice. Idempotency is mandatory, not optional.&lt;/li&gt;
&lt;li&gt;Make handlers idempotent by claiming a stable idempotency key under a unique constraint before the side effect.&lt;/li&gt;
&lt;li&gt;Ack only after success; a crash before ack triggers redelivery, which is how the system self-heals.&lt;/li&gt;
&lt;li&gt;Retry with bounded exponential backoff and jitter; route exhausted messages to a dead-letter queue and alert on it.&lt;/li&gt;
&lt;li&gt;Never assume ordering across messages unless you explicitly use a FIFO queue with a partition key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Async processing is one pillar of building backends that stay fast and reliable under load. Pair it with the broader scaling patterns and the data-consistency rules that decide what the producer must do &lt;em&gt;before&lt;/em&gt; it enqueues.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/scalability-principles" rel="noopener noreferrer"&gt;Scalability Principles&lt;/a&gt;, where queues fit among caching, load balancing, and horizontal scaling.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/database-transactions-and-consistency" rel="noopener noreferrer"&gt;Database Transactions &amp;amp; Consistency&lt;/a&gt;, why the charge and the order row must commit together before you enqueue follow-up work.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/backend-engineer" rel="noopener noreferrer"&gt;Backend Engineer path&lt;/a&gt; to see how async processing connects to the rest of the curriculum.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/async-processing-queues-and-workers" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>queues</category>
    </item>
    <item>
      <title>Authentication &amp; Sessions in Backends</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sat, 03 Oct 2026 15:00:53 +0000</pubDate>
      <link>https://dev.to/sri2614/authentication-sessions-in-backends-4kg0</link>
      <guid>https://dev.to/sri2614/authentication-sessions-in-backends-4kg0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Verifying a password is step one. The real decision is &lt;strong&gt;server sessions vs JWTs&lt;/strong&gt;, and the part most people botch is logout and revocation. Get cookies, hashing, and token revocation right or pay in hours debugged or a breach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You can verify a password. Now what?&lt;/li&gt;
&lt;li&gt;The mental model: a coat-check ticket&lt;/li&gt;
&lt;li&gt;The picture: one login, then many requests&lt;/li&gt;
&lt;li&gt;Server sessions vs stateless JWTs&lt;/li&gt;
&lt;li&gt;The code: hash, verify, then issue a ticket&lt;/li&gt;
&lt;li&gt;Cookies done right (and refresh tokens)&lt;/li&gt;
&lt;li&gt;Logout and revocation, the part people skip&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours (or a breach)&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You can verify a password. Now what?
&lt;/h2&gt;

&lt;p&gt;Your login endpoint works. A user POSTs an email and password, you check them, and the response says "welcome back." Then the very next request comes in, &lt;code&gt;GET /orders&lt;/code&gt;, and the server has no idea who is asking. HTTP is stateless: every request arrives as a stranger. The whole job of authentication in a backend is to turn one successful login into a stream of requests the server can keep trusting, &lt;strong&gt;without&lt;/strong&gt; asking for the password again.&lt;/p&gt;

&lt;p&gt;That sounds simple, and that is exactly why it goes wrong. People store passwords in plaintext. They put a secret token in &lt;code&gt;localStorage&lt;/code&gt; where any script can read it. They issue tokens that never expire and can never be revoked, so a leaked credential is valid forever. This article is the backend implementation companion to &lt;a href="https://thesimplifiedtech.com/blog/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs Authorization&lt;/a&gt;, that piece explains the &lt;em&gt;concepts&lt;/em&gt;; this one shows you how to actually build the login-and-session machinery without leaving a hole in it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Backend developers who can write a route handler but have never built auth from scratch, or have copy-pasted it and aren't sure which parts are load-bearing. You'll leave knowing how to hash a password, choose between sessions and JWTs, set cookies safely, and log a user out for real. No prior security background needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The mental model: a coat-check ticket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Authentication answers "who are you?" exactly once. Everything after that is the server recognizing a credential it already issued, so the design problem is really about that credential's lifetime, storage, and revocation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of a coat check. You hand over your coat (your password) once, at the door. In return you get a little numbered ticket. For the rest of the night you never describe your coat again, you just flash the ticket and the attendant hands your coat back. The ticket is worthless to a thief who doesn't know which coat it maps to, it's small enough to carry around, and at the end of the night it stops working.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Handing over your coat at the door&lt;/td&gt;
&lt;td&gt;POSTing your password to /login, once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The numbered ticket you get back&lt;/td&gt;
&lt;td&gt;A session id or signed JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flashing the ticket all night&lt;/td&gt;
&lt;td&gt;Every later request carries the session cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The attendant's coat rack&lt;/td&gt;
&lt;td&gt;Server-side session store (or the token's own signature)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket stops working at closing time&lt;/td&gt;
&lt;td&gt;Expiry, and the ability to revoke it early&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Logging in is a one-time exchange. The session id (or token) is the ticket you carry afterward.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture: one login, then many requests
&lt;/h2&gt;

&lt;p&gt;Here is the flow end to end. The password is verified &lt;strong&gt;once&lt;/strong&gt;; from then on, each request carries the ticket and the server only has to validate the ticket, a much cheaper, much safer operation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPkJyb3dzZXIgLyBhcHAiKQogIGNsYXNzIG5fY2xpZW50IGNsaWVudDsKICBuX2FwaSgiQXV0aCBlbmRwb2ludDxici8-UE9TVCAvbG9naW4iKQogIGNsYXNzIG5fYXBpIGNvbXB1dGU7CiAgbl9oYXNoKCJWZXJpZnkgaGFzaDxici8-YmNyeXB0IC8gYXJnb24yIikKICBjbGFzcyBuX2hhc2ggY29tcHV0ZTsKICBuX2lzc3VlKCJJc3N1ZSB0aWNrZXQ8YnIvPnNlc3Npb24gaWQgLyBKV1QiKQogIGNsYXNzIG5faXNzdWUgY29tcHV0ZTsKICBuX3N0b3JlKCJTZXNzaW9uIHN0b3JlPGJyLz5SZWRpcyAvIERCIikKICBjbGFzcyBuX3N0b3JlIGRhdGE7CiAgbl9wcm90ZWN0ZWQoIlByb3RlY3RlZCByb3V0ZTxici8-R0VUIC9vcmRlcnMiKQogIGNsYXNzIG5fcHJvdGVjdGVkIGNvbXB1dGU7CiAgbl9jbGllbnQgLS0-fCJlbWFpbCArIHBhc3N3b3JkInwgbl9hcGkKICBuX2FwaSAtLi0-fCJjb21wYXJlInwgbl9oYXNoCiAgbl9hcGkgLS0-fCJvbiBzdWNjZXNzInwgbl9pc3N1ZQogIG5faXNzdWUgLS4tPnwicGVyc2lzdCJ8IG5fc3RvcmUKICBuX2lzc3VlIC0tPnwiU2V0LUNvb2tpZSJ8IG5fY2xpZW50CiAgbl9jbGllbnQgLS0-fCJjb29raWUgb24gZXZlcnkgcmVxdWVzdCJ8IG5fcHJvdGVjdGVkCiAgbl9wcm90ZWN0ZWQgLS4tPnwidmFsaWRhdGUifCBuX3N0b3Jl%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPkJyb3dzZXIgLyBhcHAiKQogIGNsYXNzIG5fY2xpZW50IGNsaWVudDsKICBuX2FwaSgiQXV0aCBlbmRwb2ludDxici8-UE9TVCAvbG9naW4iKQogIGNsYXNzIG5fYXBpIGNvbXB1dGU7CiAgbl9oYXNoKCJWZXJpZnkgaGFzaDxici8-YmNyeXB0IC8gYXJnb24yIikKICBjbGFzcyBuX2hhc2ggY29tcHV0ZTsKICBuX2lzc3VlKCJJc3N1ZSB0aWNrZXQ8YnIvPnNlc3Npb24gaWQgLyBKV1QiKQogIGNsYXNzIG5faXNzdWUgY29tcHV0ZTsKICBuX3N0b3JlKCJTZXNzaW9uIHN0b3JlPGJyLz5SZWRpcyAvIERCIikKICBjbGFzcyBuX3N0b3JlIGRhdGE7CiAgbl9wcm90ZWN0ZWQoIlByb3RlY3RlZCByb3V0ZTxici8-R0VUIC9vcmRlcnMiKQogIGNsYXNzIG5fcHJvdGVjdGVkIGNvbXB1dGU7CiAgbl9jbGllbnQgLS0-fCJlbWFpbCArIHBhc3N3b3JkInwgbl9hcGkKICBuX2FwaSAtLi0-fCJjb21wYXJlInwgbl9oYXNoCiAgbl9hcGkgLS0-fCJvbiBzdWNjZXNzInwgbl9pc3N1ZQogIG5faXNzdWUgLS4tPnwicGVyc2lzdCJ8IG5fc3RvcmUKICBuX2lzc3VlIC0tPnwiU2V0LUNvb2tpZSJ8IG5fY2xpZW50CiAgbl9jbGllbnQgLS0-fCJjb29raWUgb24gZXZlcnkgcmVxdWVzdCJ8IG5fcHJvdGVjdGVkCiAgbl9wcm90ZWN0ZWQgLS4tPnwidmFsaWRhdGUifCBuX3N0b3Jl%3FbgColor%3D0d1017%26type%3Dpng" alt="Login verifies the password hash once; later requests carry and validate a session id or token." width="969" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Login verifies the password hash once; later requests carry and validate a session id or token.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client submits credentials&lt;/strong&gt;: The browser POSTs email + password to /login over HTTPS. This is the only request where the raw password travels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server verifies the password hash&lt;/strong&gt;: Look up the user, then compare the submitted password against the stored hash with bcrypt or argon2. You never decrypt, hashes are one-way. The library tells you match or no-match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server issues a ticket&lt;/strong&gt;: On success, mint a credential: either a random session id stored server-side, or a signed JWT the client holds. Attach it to the response as a cookie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client carries the ticket&lt;/strong&gt;: The browser stores the cookie and automatically attaches it to every subsequent request to your domain, no app code required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server validates each request&lt;/strong&gt;: For a session id, look it up in the store and confirm it's live. For a JWT, verify the signature and expiry. If it checks out, you know who's asking.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Server sessions vs stateless JWTs
&lt;/h2&gt;

&lt;p&gt;There are two ways to make a ticket the server trusts. With &lt;strong&gt;server sessions&lt;/strong&gt;, the ticket is just a random id; the real data lives in a store on your side (Redis, a database table). With &lt;strong&gt;stateless JWTs&lt;/strong&gt;, the ticket &lt;em&gt;is&lt;/em&gt; the data, a signed blob the client holds, which the server trusts because the signature can't be forged without your secret key. The trade-off is the whole game, and it comes down to one question: &lt;strong&gt;how do you revoke?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Server sessions&lt;/th&gt;
&lt;th&gt;Stateless JWT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where state lives&lt;/td&gt;
&lt;td&gt;Server store (Redis/DB); cookie holds only an opaque id&lt;/td&gt;
&lt;td&gt;Inside the token itself; server stores nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation&lt;/td&gt;
&lt;td&gt;Instant, delete the row and the next request fails&lt;/td&gt;
&lt;td&gt;Hard, valid until it expires unless you add a denylist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;Every node needs to reach the shared store&lt;/td&gt;
&lt;td&gt;Any node can verify with just the signing key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-request cost&lt;/td&gt;
&lt;td&gt;A store lookup on each request&lt;/td&gt;
&lt;td&gt;A signature check (CPU only, no I/O)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size on the wire&lt;/td&gt;
&lt;td&gt;Tiny id (~32 bytes)&lt;/td&gt;
&lt;td&gt;Bigger, header + claims + signature, sent every request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Most web apps; anything needing instant logout&lt;/td&gt;
&lt;td&gt;Short-lived API/service tokens, paired with a refresh token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Server sessions vs JWTs, the practical trade-offs.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Default to sessions:&lt;/strong&gt; If you're unsure, use server sessions. "Stateless" sounds like less work, but the moment you need to log someone out, ban an account, or invalidate everything after a password change, statelessness becomes the problem you have to engineer around. Reach for JWTs when you genuinely can't share a session store, for example, tokens passed between independently deployed services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The code: hash, verify, then issue a ticket
&lt;/h2&gt;

&lt;p&gt;Below is the core of a login flow in Python. Step one is registration, we hash the password before it ever touches the database. Step two is login, we verify against that hash, then mint a session. Notice we never store, log, or compare the raw password directly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auth.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="c1"&gt;# --- Registration: hash before storing. NEVER store the raw password. ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# bcrypt auto-generates a random salt and embeds it in the output.
&lt;/span&gt;    &lt;span class="c1"&gt;# The cost factor (12) makes each guess deliberately slow for attackers.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hashpw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gensalt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# users table stores: id, email, password_hash (the bytes above)
&lt;/span&gt;
&lt;span class="c1"&gt;# --- Login: verify against the stored hash. ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Constant-time compare; safe against timing attacks. Returns True/False.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Issue a server-side session (the coat-check ticket). ---
&lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# in real life: Redis, with a TTL
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# 256 bits of randomness, unguessable
&lt;/span&gt;    &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                        &lt;span class="c1"&gt;# expired or revoked -&amp;gt; reject
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;revoke_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# logout = delete it. Done.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather go stateless, the issue/validate pair becomes a JWT. The shape is the same, mint on login, check on each request, but there's no store, so revocation is no longer free:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jwt_auth.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;   &lt;span class="c1"&gt;# PyJWT
&lt;/span&gt;
&lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;load-this-from-an-env-var-not-source-code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;issue_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# short life: 15 minutes
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Verifies the signature AND the exp claim in one call.
&lt;/span&gt;        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidTokenError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   &lt;span class="c1"&gt;# bad signature or expired -&amp;gt; reject
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Pin the algorithm:&lt;/strong&gt; Always pass an explicit &lt;code&gt;algorithms=[...]&lt;/code&gt; allow-list when decoding a JWT. Libraries that accept the algorithm from the token's own header have shipped catastrophic bugs, an attacker sets &lt;code&gt;alg: none&lt;/code&gt; and the signature check is skipped entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Cookies done right (and refresh tokens)
&lt;/h2&gt;

&lt;p&gt;Whichever ticket you chose, deliver it in a cookie, and three flags decide whether that cookie is safe. Getting these wrong is how a working login becomes a vulnerability.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HttpOnly&lt;/strong&gt;, JavaScript cannot read the cookie. This is the single most important flag: it means a cross-site scripting (XSS) bug can't steal the session. (It's also exactly why you should &lt;em&gt;not&lt;/em&gt; keep tokens in &lt;code&gt;localStorage&lt;/code&gt;, that's readable by any script.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt;, the cookie is only sent over HTTPS, so it never travels in plaintext where it could be sniffed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SameSite&lt;/strong&gt;, controls whether the cookie rides along on cross-site requests. &lt;code&gt;Lax&lt;/code&gt; is a sane default and blocks most CSRF; &lt;code&gt;Strict&lt;/code&gt; is tighter; only use &lt;code&gt;None&lt;/code&gt; (with &lt;code&gt;Secure&lt;/code&gt;) when you genuinely need cross-site cookies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max-Age / Expires&lt;/strong&gt;, give every cookie a finite lifetime. A ticket that never expires is a permanent key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;set_cookie.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Flask example, the same flags exist in every framework.
&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;httponly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# JS can't touch it -&amp;gt; XSS can't steal it
&lt;/span&gt;    &lt;span class="n"&gt;secure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# HTTPS only
&lt;/span&gt;    &lt;span class="n"&gt;samesite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# CSRF protection
&lt;/span&gt;    &lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# finite lifetime
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Refresh tokens&lt;/strong&gt; solve a tension with JWTs: you want access tokens to be short-lived (so a leak is brief), but you don't want to ask users to log in every 15 minutes. The pattern: issue a short-lived &lt;strong&gt;access token&lt;/strong&gt; plus a long-lived &lt;strong&gt;refresh token&lt;/strong&gt;. When the access token expires, the client trades the refresh token for a new one. Critically, the refresh token is stored server-side (or in a denylist), so it &lt;em&gt;can&lt;/em&gt; be revoked, which buys back the logout ability that pure JWTs lack. Keep the refresh token in an &lt;code&gt;HttpOnly&lt;/code&gt; cookie too, and rotate it on each use so a stolen one is detectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logout and revocation, the part people skip
&lt;/h2&gt;

&lt;p&gt;Logout is not "delete the cookie in the browser." A stolen cookie still works if the server still honors it. Real logout means &lt;strong&gt;invalidating the ticket server-side&lt;/strong&gt;. With sessions this is one line, delete the row. With pure JWTs you can't, which is the whole catch: the token stays valid until it expires no matter what you do. That's why production JWT setups keep them short-lived and add a denylist of revoked token ids checked on each request, at which point you've re-added the server lookup you adopted JWTs to avoid. It's a real trade-off, not a free lunch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;logout.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Session logout: invalidate the ticket, then clear the cookie.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;revoke_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# server-side: the ticket is now dead
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;

&lt;span class="c1"&gt;# "Log out everywhere" = delete every session for the user_id.
# Trivial with sessions; needs a denylist with JWTs.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes that cost hours (or a breach)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storing passwords in plaintext, or hashing with MD5/SHA-1.&lt;/strong&gt; Those hashes are fast, which is exactly wrong: attackers brute-force them billions per second. Use bcrypt or argon2, which are deliberately slow and salted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Putting tokens in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/strong&gt; It's readable by any JavaScript on the page, so one XSS bug exfiltrates every user's session. Use &lt;code&gt;HttpOnly&lt;/code&gt; cookies instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No expiry.&lt;/strong&gt; A session or token that lives forever turns a single leak into permanent access. Always set a finite lifetime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No revocation path.&lt;/strong&gt; If you can't kill a session on logout, password change, or account ban, you can't respond to a compromise. Design revocation in from day one, it's the main reason to prefer sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepting the JWT's own &lt;code&gt;alg&lt;/code&gt; header.&lt;/strong&gt; Always pin the verification algorithm; never let the token tell you how to verify itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returning different errors for "no such user" vs "wrong password."&lt;/strong&gt; That leaks which emails are registered. Return one generic "invalid credentials" for both.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP is stateless, auth turns one login into a stream of trusted requests via a ticket.&lt;/li&gt;
&lt;li&gt;Never store raw passwords; hash with bcrypt or argon2 (slow + salted), never MD5/SHA-1.&lt;/li&gt;
&lt;li&gt;Server sessions = state on your side, instant revocation. JWTs = stateless, but revocation is hard.&lt;/li&gt;
&lt;li&gt;Default to sessions; reach for JWTs only when you can't share a session store.&lt;/li&gt;
&lt;li&gt;Deliver the ticket in an &lt;code&gt;HttpOnly&lt;/code&gt; + &lt;code&gt;Secure&lt;/code&gt; + &lt;code&gt;SameSite&lt;/code&gt; cookie with a finite lifetime, never &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Refresh tokens let access tokens stay short-lived without forcing constant re-login.&lt;/li&gt;
&lt;li&gt;Logout means invalidating the ticket server-side, not just clearing the browser cookie.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Authentication answers "who are you?", the next question is "what are you allowed to do?" Read the concept companion to this piece, then see how auth fits into a hardened API surface, and continue along the Backend Engineer track.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs Authorization&lt;/a&gt;, the concepts behind everything you just built.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;Secure API Design&lt;/a&gt;, where sessions, tokens, and cookies fit into a safe API surface.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/career-paths/backend-engineer" rel="noopener noreferrer"&gt;Backend Engineer path&lt;/a&gt;, the full track this article belongs to.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/authentication-and-sessions-in-backends" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Caching Strategies: Make Backends Fast Without Melting the Database</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Fri, 02 Oct 2026 16:26:25 +0000</pubDate>
      <link>https://dev.to/sri2614/caching-strategies-make-backends-fast-without-melting-the-database-4hpi</link>
      <guid>https://dev.to/sri2614/caching-strategies-make-backends-fast-without-melting-the-database-4hpi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Stop making the database redo the same work a thousand times a second. Compare cache-aside, read-through, write-through, and write-behind, set sane TTLs, and survive the two hard problems: &lt;strong&gt;invalidation&lt;/strong&gt; and cache stampede.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your database is doing the same work a thousand times a second&lt;/li&gt;
&lt;li&gt;The one principle behind every cache&lt;/li&gt;
&lt;li&gt;What a cached request actually looks like&lt;/li&gt;
&lt;li&gt;The four patterns, and what each one costs you&lt;/li&gt;
&lt;li&gt;Cache-aside in code&lt;/li&gt;
&lt;li&gt;The two hard problems: invalidation and stampede&lt;/li&gt;
&lt;li&gt;Where caches live&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your database is doing the same work a thousand times a second
&lt;/h2&gt;

&lt;p&gt;Picture a product page. Every visitor hits the same endpoint, which runs the same query: "give me product 42 and its reviews." The product hasn't changed in three weeks. Yet your database recomputes that answer thousands of times a second, joining tables, scanning indexes, serializing rows, only to hand back the exact same bytes it handed back a millisecond ago.&lt;/p&gt;

&lt;p&gt;That is wasted work, and it is the number one reason backends fall over under load. The fix is not a bigger database. The fix is to &lt;strong&gt;remember the answer&lt;/strong&gt; so you don't have to ask again. That's caching, and getting it right is one of the highest-leverage skills a backend engineer can build.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Junior-to-mid backend engineers who keep hearing "just add a cache" and want to actually understand the patterns, the trade-offs, and the foot-guns. You should be comfortable with HTTP requests and a database query. No distributed-systems PhD required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one principle behind every cache
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A cache is a small, fast copy of data kept close to where it's needed, traded against the risk that the copy goes stale.&lt;/p&gt;

&lt;p&gt;The entire field of caching, in one sentence&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else, the patterns, the TTLs, the invalidation headaches, flows from that single trade. You gain speed; you risk serving something slightly out of date. The whole craft is deciding &lt;strong&gt;how much staleness you can tolerate&lt;/strong&gt; for &lt;strong&gt;how much speed&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The basement archive with every file the company owns&lt;/td&gt;
&lt;td&gt;Your database, complete, durable, slow to walk to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The handful of folders you keep on your desk&lt;/td&gt;
&lt;td&gt;The cache, tiny, instant to grab, holds only what's hot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Walking to the basement when a file isn't on your desk&lt;/td&gt;
&lt;td&gt;A cache miss, fall back to the database, then bring a copy up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A desk folder going out of date after someone edits the master&lt;/td&gt;
&lt;td&gt;Stale cache, the hard part: knowing when to throw the copy away&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;A cache is just keeping the files you use most on your desk.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a cached request actually looks like
&lt;/h2&gt;

&lt;p&gt;The classic flow is the &lt;strong&gt;cache-aside&lt;/strong&gt; read. Before touching the database, ask the cache. If it has the answer (a hit), serve it. If it doesn't (a miss), fetch from the database, drop a copy in the cache for next time, then serve. Here's that path:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPkhUVFAgcmVxdWVzdCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fYXBwKCJBcHAgU2VydmVyPGJyLz5DYWNoZS1hc2lkZSBsb2dpYyIpCiAgY2xhc3Mgbl9hcHAgY29tcHV0ZTsKICBuX2NhY2hlKCJDYWNoZTxici8-UmVkaXMsIGhvdCBrZXlzIikKICBjbGFzcyBuX2NhY2hlIHF1ZXVlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5Tb3VyY2Ugb2YgdHJ1dGgiKQogIGNsYXNzIG5fZGIgZGF0YTsKICBuX2NsaWVudCAtLT58IkdFVCAvcHJvZHVjdC80MiJ8IG5fYXBwCiAgbl9hcHAgLS0-fCIxLiBjaGVjayBrZXkifCBuX2NhY2hlCiAgbl9jYWNoZSAtLi0-fCIyYS4gaGl0IOKGkiByZXR1cm4ifCBuX2FwcAogIG5fYXBwIC0tPnwiMmIuIG1pc3Mg4oaSIHF1ZXJ5Inwgbl9kYgogIG5fZGIgLS4tPnwiMy4gcm93cyJ8IG5fYXBwCiAgbl9hcHAgLS4tPnwiNC4gcG9wdWxhdGUgKFRUTCkifCBuX2NhY2hl%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPkhUVFAgcmVxdWVzdCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fYXBwKCJBcHAgU2VydmVyPGJyLz5DYWNoZS1hc2lkZSBsb2dpYyIpCiAgY2xhc3Mgbl9hcHAgY29tcHV0ZTsKICBuX2NhY2hlKCJDYWNoZTxici8-UmVkaXMsIGhvdCBrZXlzIikKICBjbGFzcyBuX2NhY2hlIHF1ZXVlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5Tb3VyY2Ugb2YgdHJ1dGgiKQogIGNsYXNzIG5fZGIgZGF0YTsKICBuX2NsaWVudCAtLT58IkdFVCAvcHJvZHVjdC80MiJ8IG5fYXBwCiAgbl9hcHAgLS0-fCIxLiBjaGVjayBrZXkifCBuX2NhY2hlCiAgbl9jYWNoZSAtLi0-fCIyYS4gaGl0IOKGkiByZXR1cm4ifCBuX2FwcAogIG5fYXBwIC0tPnwiMmIuIG1pc3Mg4oaSIHF1ZXJ5Inwgbl9kYgogIG5fZGIgLS4tPnwiMy4gcm93cyJ8IG5fYXBwCiAgbl9hcHAgLS4tPnwiNC4gcG9wdWxhdGUgKFRUTCkifCBuX2NhY2hl%3FbgColor%3D0d1017%26type%3Dpng" alt="A read with cache-aside: hits skip the database entirely; misses populate the cache on the way back." width="761" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A read with cache-aside: hits skip the database entirely; misses populate the cache on the way back.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look in the cache first&lt;/strong&gt;: Build a stable key like product:42 and ask the cache for it. This is a single fast network round-trip to Redis, typically under a millisecond.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a hit, serve and stop&lt;/strong&gt;: If the value is there, deserialize it and return. The database never hears about this request. This is the whole point, the hot path costs almost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a miss, fall back to the database&lt;/strong&gt;: The key isn't cached (first request, or it expired). Run the real query against the database to get the authoritative answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Populate the cache with a TTL&lt;/strong&gt;: Write the fresh value back into the cache with an expiry (say, 300 seconds) so the next reader gets a hit. The TTL is your safety net against staleness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return to the client&lt;/strong&gt;: Serve the answer. The first reader paid the full cost; everyone for the next five minutes rides for free.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The four patterns, and what each one costs you
&lt;/h2&gt;

&lt;p&gt;Reads and writes can each route through the cache in different ways. Four patterns dominate. The split that matters most: does &lt;strong&gt;your application code&lt;/strong&gt; manage the cache (cache-aside), or does the &lt;strong&gt;cache layer itself&lt;/strong&gt; sit in front of the database and manage it for you (read-through / write-through / write-behind)?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache-aside&lt;/td&gt;
&lt;td&gt;App checks cache; on miss, app queries DB and populates the cache itself. App owns the logic.&lt;/td&gt;
&lt;td&gt;Simple and flexible, but every reader duplicates the miss logic, and a write must remember to invalidate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-through&lt;/td&gt;
&lt;td&gt;App only talks to the cache; the cache loads from the DB on a miss transparently.&lt;/td&gt;
&lt;td&gt;Cleaner app code, but you need a cache that supports a loader, and first-read latency still hits the DB.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-through&lt;/td&gt;
&lt;td&gt;Writes go to the cache and the DB synchronously, in one operation.&lt;/td&gt;
&lt;td&gt;Cache is always fresh, but every write pays both write latencies, slower writes for guaranteed consistency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-behind&lt;/td&gt;
&lt;td&gt;Writes hit the cache instantly; the cache flushes to the DB asynchronously later.&lt;/td&gt;
&lt;td&gt;Blazing-fast writes, but a crash before flush loses data, and the DB is briefly behind reality.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The four core caching patterns. Choose by how much consistency you need and how much write latency you can spend.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For most read-heavy services, &lt;strong&gt;cache-aside is the default&lt;/strong&gt;, it's explicit, easy to reason about, and the cache failing just means slower requests, not broken ones. Reach for write-through when freshness is non-negotiable, and write-behind only when write throughput is the bottleneck and you can tolerate a small window of risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache-aside in code
&lt;/h2&gt;

&lt;p&gt;Here's the canonical cache-aside read in Python with Redis. Note the three moving parts: a &lt;strong&gt;stable key&lt;/strong&gt;, a &lt;strong&gt;TTL on the write&lt;/strong&gt;, and &lt;strong&gt;serialization&lt;/strong&gt; (Redis stores bytes, not objects).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;products.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;

&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;  &lt;span class="c1"&gt;# 5 minutes, tune to how stale you can tolerate
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Ask the cache first
&lt;/span&gt;    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# HIT, DB never touched
&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. MISS, fall back to the source of truth
&lt;/span&gt;    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Populate the cache with a TTL so the next reader hits
&lt;/span&gt;    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TTL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Invalidate, don't trust the old copy. Next read re-populates.
&lt;/span&gt;    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write path is where most bugs live. After updating the database, we &lt;strong&gt;delete&lt;/strong&gt; the cached key rather than trying to overwrite it. Deleting is safer: the next read re-populates from the authoritative source, so you never risk writing a half-built or out-of-order value into the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two hard problems: invalidation and stampede
&lt;/h2&gt;

&lt;p&gt;There's an old joke that there are only two hard things in computer science: cache invalidation, naming things, and off-by-one errors. The joke is funny because invalidation really is brutal. A cache is a &lt;strong&gt;promise that the copy still matches the source&lt;/strong&gt;, and the moment that promise breaks silently, you're serving wrong data to real users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invalidation: knowing when to throw the copy away
&lt;/h3&gt;

&lt;p&gt;You have two tools. &lt;strong&gt;TTL&lt;/strong&gt; (time-to-live) is the lazy, reliable one: every entry self-destructs after N seconds, so staleness is bounded even if you forget everything else. &lt;strong&gt;Explicit invalidation&lt;/strong&gt; is the precise one: when data changes, delete the matching key right then. Real systems use both, a short-ish TTL as a backstop, plus explicit deletes on writes for freshness where it matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stampede: the thundering herd
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;cache stampede&lt;/strong&gt; (or thundering herd) happens when a hot key expires and a thousand concurrent requests all miss at the same instant. They all fall through to the database simultaneously, each running the expensive query the cache was supposed to prevent, and the database, hit with a thousandfold spike, melts. The fix: a short &lt;strong&gt;lock&lt;/strong&gt; so only one request recomputes while the others wait, or &lt;strong&gt;staggered TTLs&lt;/strong&gt; (add a little random jitter) so keys don't all expire on the same tick.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;A cache is not a database:&lt;/strong&gt; Caches are allowed to lose data, that's the deal you signed for speed. Never make correctness depend on a value being in the cache. If Redis restarts and forgets everything, your service should get slower, not wrong. Always design the miss path to produce the correct answer on its own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where caches live
&lt;/h2&gt;

&lt;p&gt;"The cache" isn't one place. It's a spectrum from closest-and-smallest to farthest-and-biggest, and serious systems layer several:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-process&lt;/strong&gt;, a hash map or LRU inside your app's own memory. Nanosecond-fast, zero network, but each server has its own copy (no sharing) and it dies on restart. Great for tiny, hot, rarely-changing data like feature flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed (Redis / Memcached)&lt;/strong&gt;, a separate service all your app servers share. One network hop, survives app restarts, and one invalidation reaches everyone. This is the workhorse for cache-aside. Redis adds data structures and persistence; Memcached is leaner and pure key-value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN / edge&lt;/strong&gt;, caches whole HTTP responses (images, pages, API results) in data centers near the user. The request never reaches your servers at all on a hit. Ideal for public, cacheable content; controlled with Cache-Control headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These compose. A request might check an in-process cache, then Redis, then the database, each layer catching what the one before it missed. The closer the layer, the faster and the smaller. Start with one shared Redis; reach for the others when the numbers tell you to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No TTL.&lt;/strong&gt; A cache entry with no expiry can outlive its truth forever. If your invalidation has a single bug, you serve that stale value until the heat death of the universe. Always set a TTL as a backstop, even when you also invalidate explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching everything.&lt;/strong&gt; Caching data that's read once, or changes every second, just adds a network hop and a consistency risk for zero benefit. Cache what's &lt;strong&gt;hot and stable&lt;/strong&gt;, not what's convenient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serving stale data silently.&lt;/strong&gt; Updating the database but forgetting to invalidate the cache is the classic correctness bug. Every write path must either delete or refresh the matching key, make it part of the write, not an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the thundering herd.&lt;/strong&gt; A naive cache works fine in dev and stampedes the database the first time a popular key expires under real traffic. Add a lock or TTL jitter to hot keys before they bite you in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the cache for correctness.&lt;/strong&gt; Treating a cache as durable storage means a Redis restart becomes a data-loss incident. The miss path must always be able to produce the right answer alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Caching in nine lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cache trades a small risk of staleness for a large win in speed.&lt;/li&gt;
&lt;li&gt;Cache-aside is the sensible default: check cache, miss falls back to DB, then populate.&lt;/li&gt;
&lt;li&gt;Read-through hides the loader; write-through keeps the cache fresh; write-behind makes writes fast but riskier.&lt;/li&gt;
&lt;li&gt;Always set a TTL, it's your backstop when invalidation has a bug.&lt;/li&gt;
&lt;li&gt;On writes, &lt;strong&gt;delete&lt;/strong&gt; the key rather than overwriting it; the next read re-populates cleanly.&lt;/li&gt;
&lt;li&gt;Cache stampede melts databases, defend hot keys with a lock or TTL jitter.&lt;/li&gt;
&lt;li&gt;Caches live in-process, in Redis/Memcached, and at the CDN edge; layer them deliberately.&lt;/li&gt;
&lt;li&gt;Cache what's hot and stable, not everything.&lt;/li&gt;
&lt;li&gt;Never make correctness depend on the cache, the miss path must stand on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Caching makes reads cheap, but it can't fix a slow query underneath, the first reader and every cache miss still pays that cost. Make the source fast too, and understand how caching fits into scaling the whole system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/database-indexing-and-query-performance" rel="noopener noreferrer"&gt;Database Indexing &amp;amp; Query Performance&lt;/a&gt;, make the miss path itself fast, so a cold cache doesn't hurt.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/scalability-principles" rel="noopener noreferrer"&gt;Scalability Principles&lt;/a&gt;, where caching sits among the broader tools for handling load.&lt;/li&gt;
&lt;li&gt;Follow the full &lt;a href="https://thesimplifiedtech.com/career-paths/backend-engineer" rel="noopener noreferrer"&gt;Backend Engineer track&lt;/a&gt; to build these skills in order.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/caching-strategies" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>caching</category>
    </item>
  </channel>
</rss>
