<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>The Missing Level</title>
    <link>https://themissinglevel.dev/</link>
    <atom:link href="https://themissinglevel.dev/rss.xml" rel="self" type="application/rss+xml"/>
    <description>Plain-English explanations of how the web works, why modern technologies exist, and the software engineering concepts every developer should understand.</description>
    <language>en</language>
    <lastBuildDate>Sat, 29 Aug 2026 10:00:00 GMT</lastBuildDate>
    <item>
      <title>Why refreshing your single page app gives a 404</title>
      <link>https://themissinglevel.dev/why-refreshing-your-single-page-app-gives-a-404/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-refreshing-your-single-page-app-gives-a-404/</guid>
      <pubDate>Sat, 29 Aug 2026 10:00:00 GMT</pubDate>
      <description>Your routes work until someone reloads or opens a link directly. Here is why the server has never heard of that URL, and how to tell it what to do.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>Everything works while you click. You move from the dashboard to the settings page, the URL in the address bar changes to <code>/settings</code>, the right screen appears, and it feels like a normal website.</p>
<p>Then you press refresh on that page and get a <code>404</code>. Or you send the link to a colleague and it fails for them. Or a user opens a bookmark and lands on your hosting provider's error page instead of your app.</p>
<p>Nothing in your routing code is wrong. The route exists, the component is correct, and the router is configured properly. The problem is that on a refresh, your router is not the thing answering the question. It has not even loaded yet.</p>
<h2 id="why-does-the-same-url-work-when-i-click-but-not-when-i-reload">Why does the same URL work when I click but not when I reload?<a class="heading-anchor" href="#why-does-the-same-url-work-when-i-click-but-not-when-i-reload" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>These are two completely different journeys, and the difference is the whole article.</p>
<p>When you click a link inside a <a href="/why-we-needed-single-page-applications/">single page application</a>, no request goes out. Your Javascript intercepts the click, calls the History API to change what the address bar displays, and swaps the component on screen. The URL changes, but the browser never asks anyone for anything. The page you were already on simply redraws itself.</p>
<p>When you press refresh, the browser throws all of that away and does what it always does with a URL, which is <a href="/what-happens-after-you-press-enter/">ask the server for it</a>. It sends a real <code>GET /settings</code> over the network, and now your application is not involved at all, because it no longer exists. It was wiped from memory the moment you hit reload.</p>
<p>So <a href="/what-is-a-server-really/">the server</a> receives a request for <code>/settings</code>, looks in the folder it is serving, and finds <code>index.html</code>, a <code>assets</code> directory and nothing else. There is no <code>settings</code> file and no <code>settings</code> folder, because your build never created one. It responds with a <code>404</code>, correctly, because from its point of view that path genuinely does not exist.</p>
<p>The route only ever existed inside Javascript that never got a chance to run.</p>
<h2 id="why-is-this-not-a-bug-in-react-router">Why is this not a bug in React Router?<a class="heading-anchor" href="#why-is-this-not-a-bug-in-react-router" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Because client side routing is, by design, a fiction that the browser maintains on your behalf.</p>
<p><code>pushState</code> lets <a href="/why-we-needed-javascript/">Javascript</a> change the address bar without making a request. That is what makes an SPA feel fast, and it is the entire trick behind <a href="/why-we-needed-react/">modern frontend frameworks</a>. But the address bar is only a display. Changing it does not create anything on the server, does not register a path anywhere, and does not survive a reload.</p>
<p>Your router can only handle a URL if it is already running. On a refresh or a direct visit, the order is reversed: the request happens first, and your router loads afterwards, if the server sends it anything at all. Every framework has this problem, whether it is React Router, Vue Router, Angular, or a router you wrote yourself in an afternoon.</p>
<h2 id="how-do-you-actually-fix-it">How do you actually fix it?<a class="heading-anchor" href="#how-do-you-actually-fix-it" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>There is one rule, and every fix below is the same rule written in a different configuration language:</p>
<p><strong>When a request does not match a real file, send <code>index.html</code> anyway, with a <code>200</code> status.</strong></p>
<p>That way, a request for <code>/settings</code> returns your application. The browser loads it, your router starts up, reads <code>/settings</code> from the address bar, and renders the right screen. The server does not need to know your routes. It just needs to hand over the app and let the app sort it out.</p>
<p>For Nginx:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>location / {</span></span>
<span class="line"><span>  try_files $uri $uri/ /index.html;</span></span>
<span class="line"><span>}</span></span></code></pre></div>
<p>That reads as "try the file, then the directory, then fall back to index.html".</p>
<p>For Apache, in <code>.htaccess</code>:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>RewriteEngine On</span></span>
<span class="line"><span>RewriteCond %{REQUEST_FILENAME} !-f</span></span>
<span class="line"><span>RewriteCond %{REQUEST_FILENAME} !-d</span></span>
<span class="line"><span>RewriteRule . /index.html [L]</span></span></code></pre></div>
<p>For an Express server hosting a build:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">app.</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(express.</span><span style="color:#B392F0">static</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"dist"</span><span style="color:#E1E4E8">));</span></span>
<span class="line"><span style="color:#E1E4E8">app.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"*"</span><span style="color:#E1E4E8">, (</span><span style="color:#FFAB70">req</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  res.</span><span style="color:#B392F0">sendFile</span><span style="color:#E1E4E8">(path.</span><span style="color:#B392F0">join</span><span style="color:#E1E4E8">(__dirname, </span><span style="color:#9ECBFF">"dist"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"index.html"</span><span style="color:#E1E4E8">));</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span></code></pre></div>
<p>The order matters here. The static middleware has to come first, otherwise the catch all swallows requests for your Javascript and CSS as well, and you will get an application that loads <code>index.html</code> for every asset it asks for.</p>
<p>Most hosting platforms have this built in or one line away. Netlify uses a <code>_redirects</code> file with <code>/* /index.html 200</code>. Vercel and Cloudflare Pages detect common frameworks and handle it automatically. On S3 with CloudFront, the usual approach is to set the error document to <code>index.html</code>, though it is worth noting that this historically returned a <code>403</code> or <code>404</code> status underneath, which search engines do notice.</p>
<h2 id="why-does-it-work-in-development-but-break-in-production">Why does it work in development but break in production?<a class="heading-anchor" href="#why-does-it-work-in-development-but-break-in-production" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Because your development server already does this for you, quietly.</p>
<p>Vite, webpack dev server and the Next.js dev server all have history API fallback turned on by default. They serve <code>index.html</code> for anything that does not match a file, precisely so that refreshing during development does not annoy you. Then you build, upload the <code>dist</code> folder to a plain static host, and the fallback disappears with it.</p>
<p>This is why the bug so reliably appears at the worst possible moment. It is not introduced by the build. It was hidden by the dev server the entire time you were working.</p>
<h2 id="why-does-the-page-load-but-every-asset-404">Why does the page load but every asset 404?<a class="heading-anchor" href="#why-does-the-page-load-but-every-asset-404" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This is the neighbouring bug, and people often meet it right after fixing the first one.</p>
<p>If you deploy your app into a subdirectory, such as <code>example.com/app/</code>, your <code>index.html</code> will ask for <code>/assets/main.js</code> starting from the domain root, and the server will not find it there. You get a blank white page, a console full of <code>404</code>s, and often a MIME type error complaining that your Javascript was served as HTML. That error is the fallback doing its job, by the way. It returned <code>index.html</code> for a missing script, exactly as instructed.</p>
<p>The fix is to tell your build where it will live. In Vite that is the <code>base</code> option, in webpack it is <code>publicPath</code>, and in Next.js it is <code>basePath</code>. Set it to <code>/app/</code> and the generated paths line up with reality.</p>
<h2 id="what-about-pages-that-really-are-missing">What about pages that really are missing?<a class="heading-anchor" href="#what-about-pages-that-really-are-missing" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Once every path returns <code>index.html</code> with a <code>200</code>, your server can no longer produce a genuine <code>404</code>. That is a real cost, and it is worth handling deliberately rather than ignoring.</p>
<p>Your router should have a catch all route that renders a proper "not found" screen, so a typo in the URL shows something sensible instead of a blank layout.</p>
<p>For search engines the situation is more awkward. A crawler asking for a mistyped or deleted URL receives a <code>200</code> and a page, which tells it the URL is valid. Enough of these and you have what is usually called a soft 404, where search engines index URLs that were never meant to exist. If those pages matter to you, the answer is server side rendering or prerendering for real routes, so that a missing one can return an honest <code>404</code> status. A client side catch all screen fixes the experience for people, but it cannot fix the status code, because by the time your Javascript decides the page is missing, the response has already been sent.</p>
<h2 id="a-checklist-to-work-through">A checklist to work through<a class="heading-anchor" href="#a-checklist-to-work-through" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ol>
<li>Does refreshing on the home page work, but refreshing on a nested route fail? Then it is this, and the fix is in your host configuration, not your router.</li>
<li>Have you added a fallback that serves <code>index.html</code> for unmatched paths?</li>
<li>Does the fallback return <code>200</code>, rather than a <code>404</code> page that happens to contain your app?</li>
<li>Is static file handling registered <strong>before</strong> the catch all, so assets are still served normally?</li>
<li>Are you deploying into a subdirectory? If yes, set <code>base</code>, <code>publicPath</code> or <code>basePath</code> to match.</li>
<li>Does your router have a catch all route for genuinely unknown URLs?</li>
<li>Does it work in <code>npm run dev</code> but not on the deployed build? That is the dev server's fallback hiding the problem.</li>
</ol>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="why-does-the-home-page-refresh-fine-but-nothing-else">Why does the home page refresh fine but nothing else?<a class="heading-anchor" href="#why-does-the-home-page-refresh-fine-but-nothing-else" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Because <code>/</code> maps to a file that genuinely exists. Your server finds <code>index.html</code> and serves it, so the app loads and the router takes over. Every other route has no matching file, so it fails at the server before your Javascript is ever involved.</p>
<h3 id="is-hash-routing-a-valid-fix">Is hash routing a valid fix?<a class="heading-anchor" href="#is-hash-routing-a-valid-fix" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>It works, and it needs no server configuration at all, because everything after the <code>#</code> is never sent to the server. Your URLs become <code>example.com/#/settings</code>, and the server only ever sees a request for <code>/</code>. The tradeoff is ugly URLs and weaker SEO, since the fragment is not part of what gets requested. It is a reasonable choice for an internal tool or an app behind a login, and a poor one for anything you want indexed.</p>
<h3 id="does-this-affect-server-side-rendered-apps">Does this affect server side rendered apps?<a class="heading-anchor" href="#does-this-affect-server-side-rendered-apps" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No, and that is one of the reasons frameworks moved back towards <a href="/why-we-needed-node-js/">rendering on the server</a>. With Next.js, Nuxt or SvelteKit in their default modes, the server knows your routes and can answer a request for <code>/settings</code> with real HTML, including a real <code>404</code> when the page does not exist. You only meet this problem when you export a purely static build and hand it to a server that knows nothing about your routing.</p>
<h3 id="why-do-i-get-a-mime-type-error-about-my-javascript">Why do I get a MIME type error about my Javascript?<a class="heading-anchor" href="#why-do-i-get-a-mime-type-error-about-my-javascript" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Almost always because the fallback returned <code>index.html</code> for a missing asset. The browser asked for a script, received HTML, and refused to execute it. Fix the asset path rather than the fallback, since the fallback is behaving correctly.</p>
<h3 id="should-the-fallback-return-200-or-404">Should the fallback return 200 or 404?<a class="heading-anchor" href="#should-the-fallback-return-200-or-404" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p><code>200</code>, for any route your application actually handles. A <code>404</code> status tells the browser and search engines the page does not exist, even if you send your app alongside it. Reserve genuine <code>404</code> responses for URLs that really are gone, which in practice means rendering on the server.</p>
<h2 id="related-reading">Related reading<a class="heading-anchor" href="#related-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li><a href="/why-we-needed-single-page-applications/">Why we needed Single Page Applications (SPAs)</a></li>
<li><a href="/what-happens-after-you-press-enter/">What happens after you press Enter</a></li>
<li><a href="/what-is-a-server-really/">What is a server? Types, Hardware, Software and the Cloud</a></li>
<li><a href="/why-we-needed-react/">Why we needed React</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why your API works in Postman but not in the browser</title>
      <link>https://themissinglevel.dev/why-your-api-works-in-postman-but-not-in-the-browser/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-your-api-works-in-postman-but-not-in-the-browser/</guid>
      <pubDate>Fri, 28 Aug 2026 05:00:00 GMT</pubDate>
      <description>The same request succeeds in Postman and fails in your app. Here is what the browser adds on top, and how to tell which of its rules you broke.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>You test an endpoint in Postman and it works perfectly. Correct status code, correct JSON, no complaints. Then you copy the same URL into your frontend, run it, and the console fills with red. Same endpoint, same method, same body, completely different outcome.</p>
<p>The natural conclusion is that something is wrong with your fetch code. Usually there is nothing wrong with your fetch code. The request is fine, and the server is fine. What changed is who is making the request.</p>
<p>Postman is not a browser. It is a program that opens a connection and sends bytes. A browser is a program that opens a connection, sends bytes, and then enforces a long list of rules designed to protect the user from the website they are currently visiting. Those rules do not exist in Postman, so Postman can never tell you whether a browser would have allowed the same call.</p>
<h2 id="what-is-postman-actually-skipping">What is Postman actually skipping?<a class="heading-anchor" href="#what-is-postman-actually-skipping" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Three things, and each one causes a different failure.</p>
<p>The first is the <strong>origin</strong>. Every request a browser makes comes from a page, and that page has an origin made of scheme, host and port. <code>https://app.example.com</code> and <code>https://api.example.com</code> are different origins, and so are <code>http://localhost:3000</code> and <code>http://localhost:5173</code>. Postman has no origin at all, because it is not on a page. So it can never be making a cross origin request, which means the whole set of cross origin rules simply does not apply to it.</p>
<p>The second is <strong>cookie handling</strong>. Postman keeps a cookie jar that ignores <code>SameSite</code>, ignores <code>Secure</code>, and cheerfully stores things a browser would throw away. I wrote about that in detail in <a href="/why-your-cookie-is-not-being-set/">why your cookie is not being set</a>, and it is the second most common cause of this exact symptom.</p>
<p>The third is <strong>the same origin policy</strong>, which is the rule underneath all of it. A page is not allowed to read the response from another origin unless that origin explicitly agrees. Without this, any website you visited could quietly read your email, because your browser would happily attach your cookies to a request to your mail provider and hand the result to whatever script asked for it.</p>
<p>So when Postman succeeds and the browser fails, the browser is usually not broken. It is doing the one job Postman was never asked to do.</p>
<h2 id="is-it-a-cors-error">Is it a CORS error?<a class="heading-anchor" href="#is-it-a-cors-error" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Open the console and look for wording like this:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Access to fetch at 'https://api.example.com/users' from origin</span></span>
<span class="line"><span>'http://localhost:3000' has been blocked by CORS policy:</span></span>
<span class="line"><span>No 'Access-Control-Allow-Origin' header is present on the requested resource.</span></span></code></pre></div>
<p>If you see that, the server never gave the browser permission to share the response with your page. The fix is on the server, which has to say who is allowed:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Access-Control-Allow-Origin: http://localhost:3000</span></span></code></pre></div>
<p>There is one detail here that saves a lot of confusion. <strong>CORS is enforced by the browser, not by the server.</strong> The server does not block anything. It simply states, in a <a href="/seven-http-request-headers-every-developer-should-understand/">response header</a>, which origins are allowed to read what it sent. The browser reads that statement and decides whether to hand the response to your JavaScript or throw it away.</p>
<p>This is why adding a CORS library to your backend feels like it "unlocks" the API. Nothing was locked. You just started answering a question the browser had been asking all along.</p>
<h2 id="why-is-there-an-options-request-i-never-wrote">Why is there an OPTIONS request I never wrote?<a class="heading-anchor" href="#why-is-there-an-options-request-i-never-wrote" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Look at the Network tab and you will often find an <code>OPTIONS</code> request sitting in front of your real one, which nobody in your codebase asked for. That is a <strong>preflight</strong>.</p>
<p>Before sending certain cross origin requests, the browser sends a small <code>OPTIONS</code> request to ask whether the real one is acceptable. It does this whenever the request is not what the specification calls simple, which in practice means any of the following:</p>
<ul>
<li>The method is something other than <code>GET</code>, <code>HEAD</code> or <code>POST</code>.</li>
<li>You set a custom header, such as <code>Authorization</code> or <code>X-Api-Key</code>.</li>
<li>The <code>Content-Type</code> is anything other than <code>application/x-www-form-urlencoded</code>, <code>multipart/form-data</code> or <code>text/plain</code>.</li>
</ul>
<p>That last one catches almost everybody, because <code>application/json</code> is not on the list. A perfectly ordinary JSON POST is preflighted, which is why so many people meet <code>OPTIONS</code> for the first time while debugging their login form.</p>
<p>The preflight asks its question with headers, and the server has to answer all of them:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Access-Control-Allow-Origin: http://localhost:3000</span></span>
<span class="line"><span>Access-Control-Allow-Methods: POST, GET, OPTIONS</span></span>
<span class="line"><span>Access-Control-Allow-Headers: Content-Type, Authorization</span></span>
<span class="line"><span>Access-Control-Max-Age: 86400</span></span></code></pre></div>
<p>If the preflight fails, the real request is never sent at all. This produces one of the strangest symptoms in web development, where your backend logs show nothing, your endpoint is definitely running, and the browser insists it tried. It did try. It asked permission first, did not like the answer, and stopped.</p>
<p><code>Access-Control-Max-Age</code> is worth setting, because it tells the browser how long it may reuse the answer instead of preflighting every single call.</p>
<h2 id="why-does-the-preflight-come-back-401-or-403">Why does the preflight come back 401 or 403?<a class="heading-anchor" href="#why-does-the-preflight-come-back-401-or-403" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This one deserves its own section, because the cause is almost always the same and it is very easy to miss.</p>
<p>An <code>OPTIONS</code> preflight is sent <strong>without credentials</strong>. No cookies, no <code>Authorization</code> header, nothing. It is a question about permission, not an attempt to do anything. If your authentication middleware runs before your CORS middleware, it sees a request with no credentials attached and rejects it with a <code>401</code>, long before anything gets a chance to answer the browser's question.</p>
<p>The result looks like a broken login, but the login was never attempted. The fix is ordering. CORS handling has to come first in your middleware chain, and the <code>OPTIONS</code> method has to be allowed through without authentication.</p>
<h2 id="why-does-the-response-arrive-but-my-code-still-fails">Why does the response arrive but my code still fails?<a class="heading-anchor" href="#why-does-the-response-arrive-but-my-code-still-fails" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Sometimes the Network tab shows a healthy <code>200</code> with a full response body, and your <code>fetch</code> still rejects. Reading that as a contradiction is understandable, but both things are true at once.</p>
<p>Unless a request is preflighted, the browser sends it, <a href="/what-is-a-server-really/">the server</a> receives it, and it does whatever it was going to do. The row you are looking at in the Network tab is real. What the browser then does is check the CORS headers on the way back, decide your page is not allowed to read the result, and reject the promise with a <code>TypeError</code> and no status code.</p>
<p>The important consequence is one that catches people out on the security side. <strong>A blocked response does not mean a blocked request.</strong> If that call created a record or charged a card, that still happened. CORS protects the user from reading data, and it was never a substitute for the server checking whether the caller was allowed to act. It is the same principle as the fact that <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a>, just seen from the other direction.</p>
<p>If you need to read a custom response header, note that the browser hides those too. The server has to list them explicitly with <code>Access-Control-Expose-Headers</code>, which is why your pagination or rate limit header looks missing even though you can see it in the Network tab.</p>
<h2 id="why-does-it-break-only-when-i-send-cookies">Why does it break only when I send cookies?<a class="heading-anchor" href="#why-does-it-break-only-when-i-send-cookies" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The moment your request carries credentials, the rules get stricter.</p>
<p>First, you have to opt in on the client, because <code>fetch</code> does not send cookies cross origin by default:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">await</span><span style="color:#B392F0"> fetch</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"https://api.example.com/me"</span><span style="color:#E1E4E8">, { credentials: </span><span style="color:#9ECBFF">"include"</span><span style="color:#E1E4E8"> });</span></span></code></pre></div>
<p>Then the server has to opt in as well, with one extra rule that surprises everybody:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Access-Control-Allow-Origin: http://localhost:3000</span></span>
<span class="line"><span>Access-Control-Allow-Credentials: true</span></span></code></pre></div>
<p><code>Access-Control-Allow-Origin: *</code> is <strong>not allowed</strong> once credentials are involved. The wildcard means "anyone may read this", and the browser refuses to combine that with a request carrying somebody's session. You have to name the exact origin, which usually means reading the incoming <code>Origin</code> header and echoing it back from a list you trust. If you do that, send <code>Vary: Origin</code> too, otherwise a cache can serve one origin's response to another.</p>
<p>And if the cookie itself is the problem rather than the CORS configuration, the request will look permitted while the user stays logged out. That is a different bug with <a href="/why-your-cookie-is-not-being-set/">its own checklist</a>, and the quickest way to tell them apart is whether the console shows a CORS message at all. No message and no session usually means the cookie never made it.</p>
<h2 id="a-checklist-to-work-through">A checklist to work through<a class="heading-anchor" href="#a-checklist-to-work-through" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Go in this order, because each step rules out the one after it:</p>
<ol>
<li>Does the console show a CORS message? If not, it is probably a cookie or <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">session</a> problem, not a CORS one.</li>
<li>Is there an <code>OPTIONS</code> request in the Network tab, and what did it return? Fix that before looking at anything else.</li>
<li>If the preflight returned <code>401</code> or <code>403</code>, move your CORS middleware above your auth middleware.</li>
<li>Does the response carry <code>Access-Control-Allow-Origin</code>, and does it match your origin exactly, including scheme and port?</li>
<li>Are you sending credentials? Then the wildcard is out and <code>Access-Control-Allow-Credentials: true</code> is required.</li>
<li>Are you trying to read a custom header? Add <code>Access-Control-Expose-Headers</code>.</li>
<li>Are your frontend and backend using the same hostname spelling, rather than mixing <code>localhost</code> and <code>127.0.0.1</code>?</li>
</ol>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="can-i-fix-cors-from-the-frontend">Can I fix CORS from the frontend?<a class="heading-anchor" href="#can-i-fix-cors-from-the-frontend" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No, and this is worth being blunt about. The permission has to come from the server that owns the data, because the entire point is that a page cannot grant itself access to another origin. Browser extensions that disable the check only change your own machine, and your users will still be blocked.</p>
<h3 id="is-a-proxy-a-legitimate-fix">Is a proxy a legitimate fix?<a class="heading-anchor" href="#is-a-proxy-a-legitimate-fix" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes, and it is the standard answer when you do not control the API. Your own backend calls the third party server to server, where none of these rules apply, and your frontend talks only to your backend on the same origin. Most dev servers, including Vite and Next.js, have a proxy option built in for exactly this during development.</p>
<h3 id="why-does-it-work-in-production-but-not-locally">Why does it work in production but not locally?<a class="heading-anchor" href="#why-does-it-work-in-production-but-not-locally" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>In production your app and API are often served from the same origin, or from a properly configured one, so no cross origin rules apply. Locally they sit on two different ports, which makes them two different origins. Anything marked <code>Secure</code> also disappears over plain HTTP, which is why local development finds these bugs first.</p>
<h3 id="does-a-cors-error-mean-my-api-is-secure">Does a CORS error mean my API is secure?<a class="heading-anchor" href="#does-a-cors-error-mean-my-api-is-secure" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not at all. CORS only stops other websites from reading responses in a user's browser. It does nothing about direct calls, which is precisely why Postman reached your endpoint without any difficulty. If an endpoint should be restricted, <a href="/how-user-authentication-works/">the server has to check that itself</a> on every request.</p>
<h3 id="why-does-postman-succeed-when-the-browser-fails">Why does Postman succeed when the browser fails?<a class="heading-anchor" href="#why-does-postman-succeed-when-the-browser-fails" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Because Postman has no origin, does not apply the same origin policy, never sends a preflight, and ignores cookie attributes. A green result in Postman proves your server works. It proves nothing about whether a browser will let your page read the answer.</p>
<h2 id="related-reading">Related reading<a class="heading-anchor" href="#related-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li><a href="/why-your-cookie-is-not-being-set/">Why your cookie is not being set</a></li>
<li><a href="/why-a-server-can-never-trust-your-browser/">Why a server can never trust your browser</a></li>
<li><a href="/seven-http-request-headers-every-developer-should-understand/">Seven HTTP request headers every developer should understand</a></li>
<li><a href="/what-is-a-server-really/">What is a server? Types, Hardware, Software and the Cloud</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why your cookie is not being set</title>
      <link>https://themissinglevel.dev/why-your-cookie-is-not-being-set/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-your-cookie-is-not-being-set/</guid>
      <pubDate>Wed, 26 Aug 2026 10:00:00 GMT</pubDate>
      <description>Your server sends the header but the browser stores nothing. Here are the reasons a cookie is silently dropped, and how to find which one is yours.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>You log in, the request returns <code>200</code>, everything looks fine. Then you open the developer tools, go to the Application tab, and the cookie is simply not there. The next request goes out without it, the server treats you as a stranger, and you are back on the login page.</p>
<p>The frustrating part is that nothing failed. There is no error in the console, no warning in the terminal, no rejected promise. The browser received the cookie, looked at it, decided it was not allowed to keep it, and threw it away without telling anyone.</p>
<p>That silence is intentional. A cookie is storage that a website asks the browser to hold on its behalf, and the browser decides whether the request is acceptable. When it says no, it just says nothing.</p>
<p>There are only a handful of reasons this happens. Once you know them, this stops being a mystery and becomes a checklist.</p>
<h2 id="first-find-out-where-the-cookie-is-actually-being-lost">First, find out where the cookie is actually being lost<a class="heading-anchor" href="#first-find-out-where-the-cookie-is-actually-being-lost" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Before changing any code, you need to answer one question: did the server never send the cookie, or did the browser refuse to store it? These are completely different problems, and people waste hours fixing the wrong one.</p>
<p>Open the Network tab, click the request that should be setting the cookie, and look at the <a href="/seven-http-request-headers-every-developer-should-understand/">response headers</a>. You are looking for a line like this:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie: sessionId=abc123xyz; HttpOnly; Path=/; SameSite=Lax</span></span></code></pre></div>
<p>If that header is not there, the problem is in your backend. Your <a href="/what-is-a-server-really/">server</a> never asked for anything to be stored, so the browser has nothing to reject. Go and check your session middleware, your response code, or whether that branch of your login handler is even running.</p>
<p>If the header <strong>is</strong> there but the Application tab is still empty, the browser received the instruction and rejected it. Everything below is about that second case.</p>
<p>Browsers usually help you here. In Chrome, the Network tab shows a small warning icon next to a blocked cookie, and hovering over it tells you which rule was broken. Look there before you start guessing.</p>
<h2 id="is-samesite-blocking-a-cross-site-cookie">Is SameSite blocking a cross-site cookie?<a class="heading-anchor" href="#is-samesite-blocking-a-cross-site-cookie" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This is the most common cause today, and it became common because the default changed.</p>
<p><code>SameSite</code> controls whether a cookie is attached to requests that come from a different site. If you do not set it at all, browsers now treat the cookie as <code>SameSite=Lax</code>. That means the cookie is sent on normal top level navigation, like clicking a link, but it is not sent on cross site requests made in the background by JavaScript.</p>
<p>So if your frontend runs on <code>app.example.com</code> and your API runs on <code>api.otherdomain.com</code>, a <code>Lax</code> cookie will not travel between them. Your login request succeeds, the cookie comes back, and it is either dropped or never sent again afterwards.</p>
<p>The fix is to say explicitly that the cookie is allowed to cross sites:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie: sessionId=abc123xyz; HttpOnly; Secure; SameSite=None</span></span></code></pre></div>
<p>There is one rule that catches everybody: <code>SameSite=None</code> is only accepted together with <code>Secure</code>. If you send <code>SameSite=None</code> without <code>Secure</code>, the browser rejects the whole cookie. Not the attribute, the entire cookie. And <code>Secure</code> means HTTPS, which is why this often works in production and fails on your machine.</p>
<p>The restriction is worth understanding rather than working around. A cookie that travels on every cross site request is what made CSRF attacks easy for years, so the browser is refusing to hand your session to a site that did not earn it. It is the same instinct behind the fact that <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a>.</p>
<h2 id="are-you-actually-sending-credentials-with-the-request">Are you actually sending credentials with the request?<a class="heading-anchor" href="#are-you-actually-sending-credentials-with-the-request" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A cookie can be stored correctly and still never leave the browser again.</p>
<p>By default, <code>fetch</code> does not send cookies to a different origin, and it does not store cookies that come back from one either. You have to ask for it:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">await</span><span style="color:#B392F0"> fetch</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"https://api.example.com/login"</span><span style="color:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#E1E4E8">  method: </span><span style="color:#9ECBFF">"POST"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">  credentials: </span><span style="color:#9ECBFF">"include"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">  headers: { </span><span style="color:#9ECBFF">"Content-Type"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"application/json"</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">  body: </span><span style="color:#79B8FF">JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">({ email, password }),</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span></code></pre></div>
<p>In axios the equivalent is <code>withCredentials: true</code>. If you use a generated API client, check whether it exposes this option, because many do not send credentials unless you configure it.</p>
<p>The server has to agree as well. For a cross origin request with credentials, it must respond with <code>Access-Control-Allow-Credentials: true</code>, and <code>Access-Control-Allow-Origin</code> must name your exact origin. The wildcard <code>*</code> is not allowed once credentials are involved.</p>
<p>Both sides need to opt in, and this is the part that most often looks like a cookie problem when it is really a configuration problem.</p>
<h2 id="is-secure-set-while-you-are-on-an-http-page">Is Secure set while you are on an http page?<a class="heading-anchor" href="#is-secure-set-while-you-are-on-an-http-page" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The <code>Secure</code> attribute tells the browser to only store and send the cookie over HTTPS. On an <code>http://</code> page, a cookie with <code>Secure</code> is dropped immediately.</p>
<p>This produces a confusing symptom: everything works in production and nothing works locally. Production is on HTTPS, your development server is on plain HTTP, and the same code behaves differently in each place.</p>
<p>Browsers do make an exception for <code>localhost</code>, which counts as a secure context even over HTTP. That exception is narrower than people expect though, and it does not cover a local network address like <code>192.168.1.40</code> or a custom hostname from your hosts file. If you test on your phone over the local network, this is very often the reason.</p>
<h2 id="does-the-domain-attribute-match-the-site-you-are-on">Does the Domain attribute match the site you are on?<a class="heading-anchor" href="#does-the-domain-attribute-match-the-site-you-are-on" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A cookie can only be set for the domain the response came from, or for a parent of it. A server on <code>api.example.com</code> can set a cookie for <code>example.com</code>, because that is its parent. It cannot set one for <code>otherdomain.com</code>, and if it tries, the browser discards the cookie.</p>
<p>The rules also work differently depending on whether you include the attribute at all:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie: sessionId=abc; Domain=example.com</span></span></code></pre></div>
<p>That cookie is available on <code>example.com</code> and on every subdomain, including <code>app.example.com</code> and <code>api.example.com</code>.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie: sessionId=abc</span></span></code></pre></div>
<p>That one, with no <code>Domain</code>, is only available on the exact host that set it. This is the mistake behind "the cookie exists but my subdomain cannot see it". Nothing is broken, the cookie was simply never shared in the first place.</p>
<h2 id="does-the-path-match-the-page-you-are-looking-at">Does the Path match the page you are looking at?<a class="heading-anchor" href="#does-the-path-match-the-page-you-are-looking-at" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p><code>Path</code> limits the cookie to one section of the site. A cookie set with <code>Path=/admin</code> is not sent when you request <code>/dashboard</code>, and it will not appear in the Application tab while you are looking at a page outside that path.</p>
<p>Some session libraries set a narrow path by default, or inherit it from the route that created the session. If your cookie appears on one page and vanishes on another, check this before anything else. In almost every case you want <code>Path=/</code>.</p>
<h2 id="why-localhost-makes-all-of-this-worse">Why localhost makes all of this worse<a class="heading-anchor" href="#why-localhost-makes-all-of-this-worse" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p><code>localhost</code> and <code>127.0.0.1</code> are two different hosts as far as cookies are concerned, even though they reach the same machine. A cookie set on one is invisible to the other. If your frontend calls <code>http://localhost:3000</code> while your browser is open on <code>http://127.0.0.1:3000</code>, you will see exactly the symptoms described in this article.</p>
<p>Ports, on the other hand, are ignored. Cookies do not isolate by port, so <code>localhost:3000</code> and <code>localhost:5173</code> share the same cookie jar. That surprises people in the opposite direction, because a stale cookie from another project can quietly interfere with the one you are debugging.</p>
<p>The practical rule is to pick one hostname and use it everywhere, in your browser, in your API base URL, and in your environment files.</p>
<h2 id="a-checklist-to-work-through">A checklist to work through<a class="heading-anchor" href="#a-checklist-to-work-through" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>When a cookie is missing, go through these in order:</p>
<ol>
<li>Is the <code>Set-Cookie</code> header present in the response? If not, the problem is in your backend.</li>
<li>Does the browser show a warning icon next to that header? Read it first.</li>
<li>Is the request cross site? If yes, you need <code>SameSite=None; Secure</code> and HTTPS.</li>
<li>Is <code>credentials: "include"</code> set on the request, and does the server allow credentials with an explicit origin?</li>
<li>Is <code>Secure</code> set while you are browsing over plain HTTP?</li>
<li>Do the <code>Domain</code> and <code>Path</code> attributes cover the page you are testing on?</li>
<li>Are you consistently on either <code>localhost</code> or <code>127.0.0.1</code>, and not mixing the two?</li>
</ol>
<p>Most missing cookie bugs are one of these seven, and they are usually found in under a minute once you start at the top instead of guessing.</p>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="why-does-my-cookie-work-in-postman-but-not-in-the-browser">Why does my cookie work in Postman but not in the browser?<a class="heading-anchor" href="#why-does-my-cookie-work-in-postman-but-not-in-the-browser" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Postman does not enforce <code>SameSite</code>, and it ignores the browser rules about <a href="/why-your-api-works-in-postman-but-not-in-the-browser/">cross origin requests and credentials</a>. It stores whatever you tell it to. A cookie working in Postman only proves your server sends the header correctly, and tells you nothing about whether a browser would accept it.</p>
<h3 id="why-can-i-not-see-my-cookie-in-javascript">Why can I not see my cookie in JavaScript?<a class="heading-anchor" href="#why-can-i-not-see-my-cookie-in-javascript" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>If the cookie was set with <code>HttpOnly</code>, <code>document.cookie</code> cannot read it, by design. The cookie is still there and it is still sent with every matching request, but scripts on the page are locked out of it. This is deliberate protection for <a href="/how-user-authentication-works/">login sessions</a>, because it means a script injected into your page cannot steal the session identifier.</p>
<h3 id="why-does-my-cookie-disappear-when-i-close-the-browser">Why does my cookie disappear when I close the browser?<a class="heading-anchor" href="#why-does-my-cookie-disappear-when-i-close-the-browser" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>That is a <a href="/session-vs-session-cookie-vs-sessionstorage/">session cookie</a>, which is any cookie set without <code>Expires</code> or <code>Max-Age</code>. The browser is doing exactly what it was asked. If you want the cookie to survive a restart, add a <code>Max-Age</code> in seconds.</p>
<h3 id="does-a-missing-cookie-mean-the-session-was-never-created">Does a missing cookie mean the session was never created?<a class="heading-anchor" href="#does-a-missing-cookie-mean-the-session-was-never-created" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not necessarily. The server may have created the session record and stored it perfectly well, and only the identifier failed to reach the browser. The record and the cookie are two separate things, which is easier to see once you understand <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">how sessions and tokens differ</a>.</p>
<h3 id="why-did-this-start-failing-without-any-code-change">Why did this start failing without any code change?<a class="heading-anchor" href="#why-did-this-start-failing-without-any-code-change" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Browsers tightened cookie defaults over several releases, and the change to treat unspecified cookies as <code>SameSite=Lax</code> broke a large number of working integrations. If a cookie stopped being stored after a browser update, and the cookie crosses sites, that default is the first thing to check.</p>
<h2 id="related-reading">Related reading<a class="heading-anchor" href="#related-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li><a href="/why-your-api-works-in-postman-but-not-in-the-browser/">Why your API works in Postman but not in the browser</a></li>
<li><a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">Cookies vs Sessions vs Tokens: How websites keep you logged in</a></li>
<li><a href="/session-vs-session-cookie-vs-sessionstorage/">Session vs session cookie vs sessionStorage</a></li>
<li><a href="/why-a-server-can-never-trust-your-browser/">Why a server can never trust your browser</a></li>
<li><a href="/seven-http-request-headers-every-developer-should-understand/">Seven HTTP request headers every developer should understand</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Session vs session cookie vs sessionStorage</title>
      <link>https://themissinglevel.dev/session-vs-session-cookie-vs-sessionstorage/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/session-vs-session-cookie-vs-sessionstorage/</guid>
      <pubDate>Wed, 19 Aug 2026 10:00:00 GMT</pubDate>
      <description>Three different things share the word session. Learn what each one stores, who can read it, and exactly what ends it.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>The word <strong>session</strong> is used for at least three different things in web development, and they have almost nothing in common.</p>
<p>One of them is data sitting on a server. Another one is a rule about when a cookie is deleted. The third one is a small box of text that belongs to a single browser tab and that the server never sees.</p>
<p>They share a word, and that's it. They live in different places, different code can read them, and completely different events end them. Once you separate the three, a whole category of confusing bugs stops being confusing.</p>
<p><img src="/session-versus-session-cookie-versus-sessionstorage.webp" alt="Three columns comparing the server session as a record stored on the server, the session cookie as a small labelled cookie inside the browser, and sessionStorage as a box attached to a single browser tab."></p>
<h2 id="the-server-session-data-that-never-leaves-the-server">The server session: data that never leaves the server<a class="heading-anchor" href="#the-server-session-data-that-never-leaves-the-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A <strong>server session</strong> is information the server stores about you between requests. After you <a href="/how-user-authentication-works/">log in</a>, the server writes down who you are, when you authenticated, maybe your permissions, and keeps that record in memory, in Redis, or in <a href="/what-is-a-database/">a database</a>.</p>
<p>Your browser never receives that record. It only receives a <strong>session ID</strong>, a random string that the server uses to find the record again on the next request. I go through the whole flow in <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">cookies vs sessions vs tokens</a>, so I won't repeat it here.</p>
<p>The important part for this article is where it lives. The session is on <a href="/what-is-a-server-really/">the server</a>, which means nothing your browser does can delete it. You can close every tab, quit the browser, and reinstall it, and that record is still sitting there until the server decides otherwise.</p>
<h2 id="the-session-cookie-a-rule-about-when-a-cookie-dies">The session cookie: a rule about when a cookie dies<a class="heading-anchor" href="#the-session-cookie-a-rule-about-when-a-cookie-dies" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This is the one that trips people up the most, because the name sounds like "the cookie that holds the session".</p>
<p>A <strong>session cookie</strong> is any cookie that was set without an <code>Expires</code> date and without a <code>Max-Age</code>. That's the entire definition. It's not a special type of cookie, and it says nothing about what's inside it. It only tells the browser one thing, which is "keep this until the browsing session ends, then throw it away".</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie: sessionId=abc123xyz</span></span></code></pre></div>
<p>That's a session cookie, because there is no expiry on it.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie: sessionId=abc123xyz; Max-Age=1209600</span></span></code></pre></div>
<p>That's a <strong>persistent cookie</strong> holding exactly the same value. It survives closing the browser, because now the browser has been told how long to keep it.</p>
<p>So a cookie holding a session ID can be either one. A cookie holding your language preference can also be either one. "Session" here describes the lifetime, not the content. Both kinds are sent to the server automatically with every matching request, using the <a href="/seven-http-request-headers-every-developer-should-understand/#cookie-header"><code>Cookie</code> request header</a>.</p>
<h2 id="sessionstorage-one-box-per-tab">sessionStorage: one box per tab<a class="heading-anchor" href="#sessionstorage-one-box-per-tab" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p><code>sessionStorage</code> is a browser API for storing strings, and it is the only one of the three that Javascript on the page is meant to read and write directly.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">sessionStorage.</span><span style="color:#B392F0">setItem</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"step"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"3"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">sessionStorage.</span><span style="color:#B392F0">getItem</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"step"</span><span style="color:#E1E4E8">); </span><span style="color:#6A737D">// "3"</span></span></code></pre></div>
<p>Two things make it different from everything else. The first one is that it's <strong>never sent to the server</strong>. No header carries it, and the server has no way to ask for it. If you put something in <code>sessionStorage</code>, only your own Javascript will ever see it.</p>
<p>The second one is that it's scoped to a <strong>single tab</strong>, not to the whole browser. <code>localStorage</code> is shared by every tab open on the same site, so writing a value in one tab makes it visible in the others. <code>sessionStorage</code> is not. Open the same site in a second tab and that tab gets its own empty box, and the two tabs cannot see each other's values.</p>
<p>That per-tab behaviour is what makes it useful for things like the current step of a multi step form, or a scroll position you want to restore, where two tabs genuinely should not share state.</p>
<h2 id="what-ends-a-server-session-a-session-cookie-and-sessionstorage">What ends a server session, a session cookie and sessionStorage?<a class="heading-anchor" href="#what-ends-a-server-session-a-session-cookie-and-sessionstorage" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Here is where the three finally separate in a way you can test yourself. Each action below is answered for all three, because almost no action ends all of them.</p>
<p><img src="/what-ends-a-session-tab-close-browser-close.webp" alt="A tab closing and clearing its sessionStorage, the whole browser closing and clearing session cookies, and the server session still sitting on the server underneath both, untouched."></p>
<p><strong>You refresh the page.</strong> Nothing ends. <code>sessionStorage</code> survives a reload, the cookies are still there, and the server session is untouched. This surprises people who expect a refresh to clear things, and it is the reason <code>sessionStorage</code> works for multi step forms.</p>
<p><strong>You close the tab.</strong> The <code>sessionStorage</code> for that tab is gone. Cookies are not affected at all, because cookies belong to the browser and not to a tab. The server session is untouched, so opening the site again in a new tab leaves you logged in.</p>
<p><strong>You open the same site in a second tab.</strong> The new tab gets a fresh, empty <code>sessionStorage</code>. Cookies and the server session are shared normally, which is why you are already logged in there.</p>
<p><strong>You duplicate a tab, or open a link in a new tab from the page.</strong> The new tab starts with a <strong>copy</strong> of the original tab's <code>sessionStorage</code>. It's a copy and not a link, so from that moment the two tabs drift apart independently. This one catches almost everybody at least once.</p>
<p><strong>You close the whole browser</strong>. If the site created a session cookie (a cookie without <code>Expires</code> or <code>Max-Age</code>), the browser normally deletes it when the browser session ends. On your next visit, there is no <code>session ID</code> to send, so the site treats you as logged out. But if the developer set an <code>Expires</code> or <code>Max-Age</code> attribute, the cookie is persistent and can survive the browser closing. Either way, the important point is that closing the browser does not delete the server session itself. The server-side record remains until it expires, is invalidated, or is otherwise removed.</p>
<p><strong>You close the browser, but "continue where you left off" is on.</strong> Now closing the browser ends much less than you expected. Browsers that restore your previous tabs also restore session cookies and <code>sessionStorage</code> along with them, so you come back still logged in and with your tab state intact. The same thing happens after a crash recovery. If you have ever been unable to reproduce a "logged out on restart" bug, this setting is usually why.</p>
<p><strong>You log out properly.</strong> This is the only one that ends the server session, because it's the only one the server hears about. The server deletes its own record, and normally also tells the browser to clear the cookie. Everything else on this list is something the browser did quietly on its own.</p>
<p><strong>You do nothing for long enough.</strong> The server session expires on its own schedule and stops being valid, even though your cookie is still sitting in the browser looking perfectly fine.</p>
<p><strong>The server restarts.</strong> If sessions are stored in the server's memory, they all disappear and everyone is logged out at once. If they're in Redis or a database, they survive. This is a big reason production apps rarely keep sessions in memory.</p>
<h2 id="side-by-side">Side by side<a class="heading-anchor" href="#side-by-side" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<table>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Server session</th>
<th scope="col">Session cookie</th>
<th scope="col">sessionStorage</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Lives where?</th>
<td>On the server</td>
<td>In the browser, per site</td>
<td>In the browser, per tab</td>
</tr>
<tr>
<th scope="row">Who can read it?</th>
<td>Server code only</td>
<td>Server, and Javascript unless <code>HttpOnly</code></td>
<td>Javascript on the page only</td>
</tr>
<tr>
<th scope="row">Sent to the server?</th>
<td>It's already there</td>
<td>Yes, on every matching request</td>
<td>Never</td>
</tr>
<tr>
<th scope="row">Shared between tabs?</th>
<td>Yes</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<th scope="row">Ended by closing the tab</th>
<td>No</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<th scope="row">Ended by closing the browser</th>
<td>No</td>
<td>Yes, unless tabs are restored</td>
<td>Yes, unless tabs are restored</td>
</tr>
<tr>
<th scope="row">Typical use</th>
<td>Who you are, permissions</td>
<td>Carrying the session ID</td>
<td>Temporary per tab UI state</td>
</tr>
</tbody>
</table>
<p>The row worth memorising is the second to last one. Closing a tab ends exactly one of the three, and closing the browser ends the browser's copies but never the server's record.</p>
<h2 id="which-one-should-you-reach-for">Which one should you reach for?<a class="heading-anchor" href="#which-one-should-you-reach-for" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>If the server needs to know something, it belongs in a <strong>server session</strong>, with a <strong>session cookie</strong> carrying the ID. Mark that cookie <code>HttpOnly</code>, <code>Secure</code> and <a href="/why-your-cookie-is-not-being-set/"><code>SameSite</code></a>, because <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a> with data it intends to believe later.</p>
<p>If only the page needs it, and only in this tab, and it would be fine to lose it, use <strong><code>sessionStorage</code></strong>. A wizard step, a draft filter, a scroll position.</p>
<p>What you should not do is put a session ID or a token in <code>sessionStorage</code> and treat it as secure. It is not protected from cross site scripting in any way, since any script running on your page can read it. Being cleared when the tab closes feels like a security feature, but it's a lifetime feature, and it does nothing against the attack that actually matters.</p>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p><strong>Is a session cookie the same as a session ID?</strong></p>
<p>No. The session ID is the value, and the session cookie is the envelope with no expiry date on it. You can send a session ID in a persistent cookie, and you can send something completely unrelated in a session cookie.</p>
<p><strong>Does closing the browser log me out?</strong></p>
<p>It usually looks that way, but the server session is still alive. Your browser simply dropped the session cookie, so it stopped identifying you. If tab restore is enabled, even that doesn't happen and you come back logged in.</p>
<p><strong>Why is my sessionStorage empty in a new tab, but full in a duplicated one?</strong></p>
<p>Because duplicating a tab copies the box, while opening a new tab creates an empty one. It's the intended behaviour, and it's the most common source of confusion with <code>sessionStorage</code>.</p>
<p><strong>Can the server read sessionStorage?</strong></p>
<p>Never, unless your own Javascript reads it and sends the value in a request. There is no header and no mechanism that exposes it automatically.</p>
<h2 id="related-reading">Related reading<a class="heading-anchor" href="#related-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li><a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">Cookies vs Sessions vs Tokens: How websites keep you logged in</a></li>
<li><a href="/why-a-server-can-never-trust-your-browser/">Why a server can never trust your browser</a></li>
<li><a href="/seven-http-request-headers-every-developer-should-understand/">Seven HTTP request headers every developer should understand</a></li>
<li><a href="/why-your-cookie-is-not-being-set/">Why your cookie is not being set</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>How does a database store data on disk? Pages, logs and indexes</title>
      <link>https://themissinglevel.dev/how-a-database-stores-data-on-disk/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/how-a-database-stores-data-on-disk/</guid>
      <pubDate>Wed, 19 Aug 2026 06:00:00 GMT</pubDate>
      <description>How does a database store data on disk? Learn what pages are, why the write-ahead log exists, how crash recovery works, and what an index physically is.</description>
      <category>Software Engineering</category>
      <content:encoded><![CDATA[<p>A <a href="/what-is-a-database/">database gives you guarantees that plain files cannot</a>: a transaction applies fully or not at all, two concurrent writes do not silently overwrite each other, and a committed change survives the power going out. Those guarantees are the reason the thing exists.</p>
<p>What that explanation leaves open is how any of it is possible, because underneath there is no magic storage layer. The database is writing to the same filesystem your JSON file was on, with the same disk and the same ways of failing. Something has to be different about how it writes, and that something is the whole subject of this article.</p>
<p>This is the layer most developers never look at, which is a shame, because it explains a surprising number of things that otherwise look arbitrary. Why committing a large transaction is fast. Why adding an index slows down your inserts. Why "the database is slow" is so often a memory problem rather than a disk one.</p>
<h2 id="it-really-is-still-files">It really is still files<a class="heading-anchor" href="#it-really-is-still-files" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Start with the deflating part. If you open a PostgreSQL data directory you find files and folders. Every table is a file, or several once it grows past a size limit. A SQLite database is one single file that you can copy with <code>cp</code> and email to someone, which is a large part of why it ended up inside phones, browsers and aeroplanes.</p>
<p>So the difference between a database and the <code>users.json</code> you would have written yourself is not where the bytes live. It is the discipline with which they are put there, and that discipline comes down to three ideas: the file is divided into blocks, changes are written down before they are applied, and a second sorted structure exists so nothing has to be scanned.</p>
<h2 id="pages-not-one-big-blob">Pages, not one big blob<a class="heading-anchor" href="#pages-not-one-big-blob" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The first decision is that the file is not one continuous document. It is divided into fixed-size blocks called <strong>pages</strong>, usually 4 or 8 kilobytes each, and rows are packed into them.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>   users table file</span></span>
<span class="line"><span> ┌──────────┬──────────┬───────────┬──────────┐</span></span>
<span class="line"><span> │  page 0  │  page 1  │  page 2   │  page 3  │</span></span>
<span class="line"><span> │ rows 1-42│rows 43-88│rows 89-131│   free   │</span></span>
<span class="line"><span> └──────────┴──────────┴───────────┴──────────┘</span></span>
<span class="line"><span>      8 KB      8 KB       8 KB       8 KB</span></span></code></pre></div>
<p>This one choice solves the rewrite problem at the physical level. To change a single row, the engine reads the one page holding it, edits it in memory, and writes that page back. Your JSON file had to be serialised and rewritten in full for a one-character change, because there was no way to address a part of it. A page is the smallest unit the database ever reads or writes, and the size is not arbitrary either: disks, filesystems and databases all converge on similar block sizes because the hardware underneath transfers data in blocks anyway.</p>
<p><img src="/database-pages-on-disk.webp" alt="A table file divided into fixed size pages with rows packed inside each one, showing a single page being read into memory, modified and written back while the rest of the file is untouched."></p>
<h3 id="the-buffer-pool">The buffer pool<a class="heading-anchor" href="#the-buffer-pool" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Pages also give memory a natural unit to work with. The engine keeps recently used pages in a region of RAM called the <strong>buffer pool</strong>, so a row that was read a moment ago is served without touching the disk at all. Reads check the pool first, and only pages that are not there cause actual disk activity.</p>
<p>This is where a lot of real-world database performance actually lives. A well-tuned database serves the large majority of its reads from memory, and the phrase "the database is slow" often means nothing more exotic than the working set having grown past what the buffer pool can hold. The queries did not change, the data did, and suddenly reads that were memory lookups became disk reads.</p>
<p>Writes go through the pool too. A modified page sits in memory marked as dirty, and gets written back to the table file later rather than immediately. Which raises the obvious question: if the change is only in memory, what happens when the machine dies?</p>
<h2 id="the-write-ahead-log">The write-ahead log<a class="heading-anchor" href="#the-write-ahead-log" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Writing pages back in place is the dangerous moment. A crash halfway through leaves a page that is neither the old version nor the new one, and a half-written page is worse than a lost one, because nothing about it announces that it is broken.</p>
<p>The solution is to not touch the real pages first. Before changing anything, the engine appends a description of the change to a <strong>write-ahead log</strong>, a file that is only ever written to at the end, and waits for the disk to confirm that the append is physically durable. That confirmation is the <code>fsync</code> call, and it is the moment the promise becomes real. Only after it returns does the database report success to you.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>  COMMIT</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span>  append change to the write-ahead log</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span>  fsync            ← the disk confirms it is really there</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span>  report success to the client</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span>  update the table pages later, in the background</span></span></code></pre></div>
<h3 id="why-this-is-fast-rather-than-slow">Why this is fast rather than slow<a class="heading-anchor" href="#why-this-is-fast-rather-than-slow" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>It looks like extra work, and it is, but it is cheap work in the right place. An append to the end of one file is close to the least expensive thing you can ask a disk to do, because it is sequential and touches one location. Updating the actual pages is expensive by comparison, since a transaction can dirty pages scattered all over a large table file.</p>
<p>The log lets the database pay the cheap cost on the critical path, while you are waiting, and defer the expensive one to a background process that can batch and reorder it. That is why committing a transaction returns faster than the amount of work it implies, and it is the same trick that makes a database with careful durability guarantees outperform a naive implementation with none.</p>
<h3 id="crash-recovery">Crash recovery<a class="heading-anchor" href="#crash-recovery" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Now the payoff. If the machine dies at any point after that fsync, the log still contains every committed change that had not yet made it into the table files. On startup the engine reads the log and replays them, so nothing acknowledged is lost. It also finds transactions that were partway through with no commit record, and reverses their effects, so nothing half-finished survives either.</p>
<p>That single pass is why a database comes back consistent after a power cut and your JSON file does not. It is also, concretely, what the durability in ACID buys you, and why turning off synchronous commits for speed is a real decision with a real cost rather than a free tuning win.</p>
<p><img src="/write-ahead-log-and-crash-recovery.webp" alt="A commit appending to the write-ahead log and being confirmed by fsync before success is returned, with table pages updated separately in the background, and a restart arrow replaying the log to recover committed changes."></p>
<h2 id="what-an-index-is-on-disk">What an index is on disk<a class="heading-anchor" href="#what-an-index-is-on-disk" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The last piece of the layout is the one that makes queries fast. An <strong>index</strong> is a second structure, stored in its own pages, holding the values of one column in sorted order alongside pointers to where the full rows live.</p>
<p>Sorted order is the entire point, because it means a lookup can halve the search space repeatedly instead of walking through everything. Most indexes are a <strong>B-tree</strong>, a deliberately shallow tree whose nodes are pages, so finding one row among ten million is typically three or four page reads rather than ten million comparisons.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>                 [ M ]</span></span>
<span class="line"><span>            ┌──────┴──────┐</span></span>
<span class="line"><span>          [ F ]         [ T ]</span></span>
<span class="line"><span>        ┌───┴───┐     ┌───┴───┐</span></span>
<span class="line"><span>     A..E     G..L  N..S     U..Z</span></span>
<span class="line"><span>      ↓                        ↓</span></span>
<span class="line"><span>   row pointers into the table pages</span></span></code></pre></div>
<p>Three levels like that already cover an enormous number of rows, which is why an indexed lookup feels instant and the same query without one crawls. It is also why the improvement is so dramatic rather than incremental: you are not making the scan faster, you are avoiding the scan.</p>
<p>The cost is worth stating plainly. Every index is more data on disk, and every insert, update or delete has to modify the indexes as well as the table. Indexes are not free speed, they are a trade of write cost and storage for read cost, and how to choose them well is a subject that deserves its own article.</p>
<p>If you would rather see all of this than take my word for it, SQLite is the readable version. Its <a href="https://www.sqlite.org/fileformat.html">file format documentation</a> lays out the pages, the B-trees and the header byte by byte, for an engine that is one file on disk and running in more places than every other database combined.</p>
<h2 id="what-this-explains-in-practice">What this explains in practice<a class="heading-anchor" href="#what-this-explains-in-practice" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The point of knowing the layout is that a set of otherwise disconnected symptoms turn out to be the same few facts.</p>
<p>A bulk import that slows down as it runs is usually index maintenance, since every row inserted has to be placed into every index as well as the table. Dropping the indexes, loading the data and recreating them afterwards is a standard trick precisely because rebuilding an index once is cheaper than maintaining it a million times.</p>
<p>A table that keeps growing on disk after you delete rows is not a bug. Engines that keep multiple row versions for concurrency mark the old ones as dead rather than removing them immediately, leaving gaps inside pages that later writes can reuse. The space comes back to the table, not always to the filesystem, which is why database sizes tend to rise and plateau rather than shrink.</p>
<p>A query that was fast last month and is slow now, with no code change, is very often the buffer pool. The data grew past the point where the pages it needs stay resident, and reads that were memory hits became disk reads. That extra waiting lands squarely in <a href="/what-happens-after-you-press-enter/">the gap between the request and the first byte of the response</a>, which is why storage problems show up first as a slow-feeling website rather than as anything obviously database shaped. The fix is more memory, a better index, or asking for less data, and knowing the layout is what tells you which of the three you are actually looking at.</p>
<h2 id="wrapping-up">Wrapping up<a class="heading-anchor" href="#wrapping-up" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Nothing at this layer is mysterious once you see the constraint the designers were working against. A disk can only promise that small, sequential, confirmed writes have really happened, and everything else has to be built out of that one guarantee.</p>
<p>Pages exist so a change touches a small addressable unit instead of a whole file. The buffer pool exists because memory is orders of magnitude faster and pages are the natural thing to cache. The write-ahead log exists because a durable append is cheap and a durable scattered update is not. Indexes exist because sorted data can be searched without being read.</p>
<p>Four ideas, and they hold across almost every engine you will meet, relational or not. The names change and the details differ, but a storage layer that did not do these things would have to invent them.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>A database stores data in ordinary files. What differs from a hand-rolled store is the discipline of how those files are written, not the storage underneath.</li>
<li>Files are divided into fixed-size pages, typically 4 or 8 kilobytes, so changing one row rewrites one small block instead of the entire file.</li>
<li>The buffer pool keeps hot pages in memory, and a database that suddenly feels slow has often outgrown it rather than developed a query problem.</li>
<li>The write-ahead log is appended and flushed to disk before the real pages change, which is both what makes commits durable and what makes them fast.</li>
<li>Crash recovery replays committed changes from the log and reverses uncommitted ones, which is exactly what durability in ACID means.</li>
<li>An index is a sorted B-tree in its own pages, turning a scan of millions of rows into three or four page reads, paid for with disk space and slower writes.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="how-does-a-database-store-data-on-disk">How does a database store data on disk?<a class="heading-anchor" href="#how-does-a-database-store-data-on-disk" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>In ordinary files, organised into fixed-size blocks called pages, usually 4 or 8 kilobytes each. Rows are packed into pages so the engine can read or rewrite one small block rather than the whole file, indexes live in their own pages as sorted trees, and a separate write-ahead log records every change before the table pages are touched.</p>
<h3 id="what-is-a-write-ahead-log">What is a write-ahead log?<a class="heading-anchor" href="#what-is-a-write-ahead-log" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A file the database appends every change to, and flushes to disk, before updating the actual table data. Because the log is written first, a crash can never leave the tables in an unrepairable half-finished state: on restart the engine replays committed changes from the log and reverses anything uncommitted.</p>
<h3 id="what-is-a-page-in-a-database">What is a page in a database?<a class="heading-anchor" href="#what-is-a-page-in-a-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The smallest unit of data a database reads from or writes to disk, typically 4 or 8 kilobytes. Rows are stored inside pages, and the engine loads whole pages into memory rather than individual rows, which is why the size matches the block sizes used by disks and filesystems.</p>
<h3 id="what-is-a-buffer-pool">What is a buffer pool?<a class="heading-anchor" href="#what-is-a-buffer-pool" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The region of memory where a database keeps recently used pages so repeated reads do not touch the disk. Most of a healthy database's reads are served from it, and performance often falls off when the data actively being used grows larger than the pool.</p>
<h3 id="why-do-indexes-make-writes-slower">Why do indexes make writes slower?<a class="heading-anchor" href="#why-do-indexes-make-writes-slower" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Because every insert, update or delete has to modify each index as well as the table itself. One table with five indexes means six structures to keep correct on every write, which is why loading large amounts of data is often faster with the indexes dropped and rebuilt afterwards.</p>
<h3 id="is-sqlite-a-real-database">Is SQLite a real database?<a class="heading-anchor" href="#is-sqlite-a-real-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. It implements pages, B-tree indexes, transactions and crash recovery like any other engine, with the difference that it runs inside your application process instead of as a separate server. That makes it a poor fit for many machines writing at once and an excellent one for almost everything else.</p>
<h2 id="continue-reading">Continue reading<a class="heading-anchor" href="#continue-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li><a href="/what-is-a-database/">What is a database?</a>. The layer above this one: what a database guarantees, why plain files break, and the types of database engines you can choose between.</li>
<li><a href="/what-is-a-server-really/">What is a server, really?</a>. The database is one of four jobs behind a typical website, and this covers how the others fit around it.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>What is a database? Types, Transactions and Why Files Fail</title>
      <link>https://themissinglevel.dev/what-is-a-database/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/what-is-a-database/</guid>
      <pubDate>Wed, 19 Aug 2026 06:00:00 GMT</pubDate>
      <description>What is a database? Learn why plain files stop working, what transactions and constraints actually do, and how a database keeps data correct under load.</description>
      <category>Software Engineering</category>
      <content:encoded><![CDATA[<p>Every application you have ever built needed to remember something. A user signed up, an order was placed, a setting was changed, and that fact had to survive the request that created it. The moment your program exits, everything in memory is gone, so the data has to go somewhere else.</p>
<p>The usual answer is "put it in the database", and that sentence gets repeated so often that nobody stops to ask what the database is actually doing for you. I used it for years as a place where data goes, roughly a spreadsheet with a login. That picture is not wrong exactly, but it hides the interesting part, which is that almost everything a database does is there to solve a problem you would have hit anyway.</p>
<p>So let's do this the other way around. Instead of starting with tables and SQL, let's start with a file, break it, and see what has to be invented to fix it.</p>
<h2 id="what-is-a-database">What is a database?<a class="heading-anchor" href="#what-is-a-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A <strong>database</strong> is an organised collection of data, stored in a way that lets you find, change and protect it reliably while many things are using it at once.</p>
<p>The important half of that sentence is the second half. Storing data is the easy part, and your filesystem already does it. What a database adds is a set of guarantees about what happens when things go wrong: two people writing at the same moment, a process crashing halfway through an update, a query that must not read half-finished work. Everything else, the tables, the query language, the indexes, exists to serve those guarantees.</p>
<p>Worth separating one piece of vocabulary early, because it causes real confusion later. The <strong>database</strong> is the data itself. The <strong>DBMS</strong>, or database management system, is the software that owns it and answers questions about it. Postgres, MySQL, SQLite and MongoDB are database management systems. In everyday conversation people say "database" for both, which is fine, right up until someone says "the database is down" and you cannot tell whether they mean the data is corrupted or a process stopped listening.</p>
<p><img src="/database-versus-database-management-system.webp" alt="A diagram separating the two meanings of the word database, with stored data on one side as tables on disk, and the database management system on the other side as a running program that receives queries and returns rows." style="max-width:80%" class="img-narrow"></p>
<h2 id="why-not-use-plain-files">Why not use plain files?<a class="heading-anchor" href="#why-not-use-plain-files" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Here is the honest version of how most of us first solve this problem. You have some users, you have JSON, and you have a filesystem.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { readFile, writeFile } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> "node:fs/promises"</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> addUser</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">user</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> users</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">parse</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">await</span><span style="color:#B392F0"> readFile</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"users.json"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"utf8"</span><span style="color:#E1E4E8">));</span></span>
<span class="line"><span style="color:#E1E4E8">  users.</span><span style="color:#B392F0">push</span><span style="color:#E1E4E8">(user);</span></span>
<span class="line"><span style="color:#F97583">  await</span><span style="color:#B392F0"> writeFile</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"users.json"</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">(users));</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
<p>This works. It genuinely works, and for a script that runs once on your laptop it is the correct amount of engineering. It also contains four separate disasters waiting for the day something real happens to it, and walking through them is the fastest way to understand what a database is for.</p>
<h3 id="two-writes-at-the-same-time">Two writes at the same time<a class="heading-anchor" href="#two-writes-at-the-same-time" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Two requests arrive within a few milliseconds of each other. Both read the file and both get the same array of a hundred users. The first adds Ana and writes a hundred and one users. The second, working from the copy it read before Ana existed, adds Bruno and writes its own hundred and one users over the top.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>  Request A                Request B</span></span>
<span class="line"><span>      ↓                        ↓</span></span>
<span class="line"><span>  read 100 users          read 100 users</span></span>
<span class="line"><span>      ↓                        ↓</span></span>
<span class="line"><span>  add Ana                  add Bruno</span></span>
<span class="line"><span>      ↓                        ↓</span></span>
<span class="line"><span>  write 101                write 101</span></span>
<span class="line"><span>                               ↓</span></span>
<span class="line"><span>                     Ana is gone forever</span></span></code></pre></div>
<p>Nothing errored. No log line appeared. Ana filled in a signup form, saw a success message, and does not exist. This is called a <strong>lost update</strong>, and the thing that makes it genuinely nasty is that it happens more often the more successful you are, and it leaves no evidence behind.</p>
<p><img src="/lost-update-two-writes-at-once.webp" alt="Two concurrent requests reading the same list of users, each adding a different person, and the second write overwriting the first so one user silently disappears." style="max-width:90%" class="img-narrow"></p>
<h3 id="the-crash-in-the-middle">The crash in the middle<a class="heading-anchor" href="#the-crash-in-the-middle" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Now imagine the write itself is interrupted. The process is killed, the container is recycled, the machine loses power. Your file is halfway through being replaced, so what is on disk is not the old list and not the new one. It is a truncated fragment of JSON that will throw a parse error on the next read, and the previous good version is already gone.</p>
<p>A single write is bad enough. Real operations are usually several writes that only make sense together, like taking money out of one account and putting it into another. If the machine dies between the two, the money has left one place and arrived nowhere, and no amount of careful coding on your side can make two separate file writes happen as one indivisible event.</p>
<h3 id="finding-one-thing-means-reading-everything">Finding one thing means reading everything<a class="heading-anchor" href="#finding-one-thing-means-reading-everything" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>To find the user with the email address you were given, you read the entire file into memory and loop. At a hundred users that is instant. At ten million it is a very slow request that also allocates a few gigabytes of memory to answer a question about one row.</p>
<p>The fix is to keep a second structure on the side that maps email addresses to positions in the file, so you can jump straight there instead of scanning. That is an <strong>index</strong>, and it is the single biggest reason a database can answer in milliseconds what your loop answers in minutes. For now it is enough to know that the database keeps extra sorted copies of your data specifically so it never has to read all of it, and <a href="/how-a-database-stores-data-on-disk/">what that looks like on disk</a> is worth seeing once.</p>
<h3 id="relationships-have-nowhere-to-live">Relationships have nowhere to live<a class="heading-anchor" href="#relationships-have-nowhere-to-live" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Users have orders, orders have line items, line items point at products. In one JSON file you either nest everything, which means the same product description is duplicated in ten thousand orders and updating it means rewriting all of them, or you split into several files and hand-maintain the references between them. Then you delete a user and their orders are still there, pointing at somebody who no longer exists, and nothing in your system considers that an error.</p>
<h2 id="what-does-a-database-actually-give-you">What does a database actually give you?<a class="heading-anchor" href="#what-does-a-database-actually-give-you" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Every one of those failures has a name and a solution, and those solutions are what you are buying when you install Postgres instead of writing to a file.</p>
<h3 id="transactions-all-of-it-or-none-of-it">Transactions: all of it, or none of it<a class="heading-anchor" href="#transactions-all-of-it-or-none-of-it" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>transaction</strong> is a group of operations that the database treats as a single indivisible step. Either every statement inside it takes effect, or none of them do, and there is no state in between that anyone can observe.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">BEGIN</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">UPDATE</span><span style="color:#E1E4E8"> accounts </span><span style="color:#F97583">SET</span><span style="color:#E1E4E8"> balance </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> balance </span><span style="color:#F97583">-</span><span style="color:#79B8FF"> 100</span><span style="color:#F97583"> WHERE</span><span style="color:#E1E4E8"> id </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 1</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">UPDATE</span><span style="color:#E1E4E8"> accounts </span><span style="color:#F97583">SET</span><span style="color:#E1E4E8"> balance </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> balance </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 100</span><span style="color:#F97583"> WHERE</span><span style="color:#E1E4E8"> id </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 2</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">COMMIT</span><span style="color:#E1E4E8">;</span></span></code></pre></div>
<p>If the power goes out after the first update, the database does not leave you with money that vanished. On restart it notices the transaction never committed and rolls it back, and the accounts look exactly as they did before anyone tried. This is the guarantee that plain files cannot give you at any price, because the filesystem has no concept of "these two changes belong together".</p>
<p><img src="/transaction-all-or-nothing.webp" alt="A money transfer shown as a single transaction, with two account updates inside one box, an arrow to a committed state where both applied and an arrow to a rolled back state where neither did, and no possible outcome in between." style="max-width:90%" class="img-narrow"></p>
<p>Those guarantees are usually described with the acronym <strong>ACID</strong>: atomicity (all or nothing), consistency (the data obeys its rules before and after), isolation (concurrent transactions do not see each other's half-finished work) and durability (once it says committed, it survives a crash). The <a href="https://www.postgresql.org/docs/current/tutorial-transactions.html">PostgreSQL documentation on transactions</a> walks through the same bank transfer example, which tells you how central this one scenario is to the whole design.</p>
<h3 id="concurrency-many-writers-one-truth">Concurrency: many writers, one truth<a class="heading-anchor" href="#concurrency-many-writers-one-truth" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Isolation is the answer to the lost update from earlier. The database does not let two transactions read the same row, decide independently, and both win. Depending on the engine and the isolation level, it either makes the second one wait until the first has finished, or lets both proceed optimistically and refuses to commit the one whose assumptions turned out to be stale.</p>
<p>You still have to ask for the right behaviour. Reading a balance in one statement and writing a new one in another is the same race you had with files, even inside a transaction, which is why <code>UPDATE accounts SET balance = balance - 100</code> is safer than reading the balance into your application and sending back a number you calculated yourself. The database can only protect what it can see, so the more of the logic you express as a single statement, the more of it is covered.</p>
<h3 id="durability-what-committed-really-means">Durability: what committed really means<a class="heading-anchor" href="#durability-what-committed-really-means" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>When a database returns success on a commit, the change is already somewhere that survives a crash. Not necessarily in the table itself, which might still be sitting in memory waiting to be written out, but in a record on disk that is enough to reconstruct it. That record is called the write-ahead log, and it is the trick that lets a database be fast and safe at the same time rather than trading one for the other.</p>
<h3 id="a-query-language-instead-of-a-loop">A query language instead of a loop<a class="heading-anchor" href="#a-query-language-instead-of-a-loop" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>With files, the code that answers a question is your code. You read everything, filter, sort, count, and if you want the ten most recent orders per customer you write that yourself and hope it is efficient. <strong>SQL</strong> inverts that. You describe the result you want, and the engine's query planner decides how to get it, which index to use, which order to join the tables in, whether to sort or hash.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">SELECT</span><span style="color:#79B8FF"> u</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">email</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">COUNT</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">o</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">id</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">AS</span><span style="color:#E1E4E8"> orders</span></span>
<span class="line"><span style="color:#F97583">FROM</span><span style="color:#E1E4E8"> users u</span></span>
<span class="line"><span style="color:#F97583">LEFT JOIN</span><span style="color:#E1E4E8"> orders o </span><span style="color:#F97583">ON</span><span style="color:#79B8FF"> o</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">user_id</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> u</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">id</span></span>
<span class="line"><span style="color:#F97583">GROUP BY</span><span style="color:#79B8FF"> u</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">email</span></span>
<span class="line"><span style="color:#F97583">HAVING</span><span style="color:#79B8FF"> COUNT</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">o</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">id</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">></span><span style="color:#79B8FF"> 5</span><span style="color:#E1E4E8">;</span></span></code></pre></div>
<p>That question, asked over millions of rows, would be a genuinely difficult program to write by hand and an even harder one to keep fast. Here it is five lines, and the planner rewrites the strategy on its own as the data grows and the shape of the tables changes.</p>
<h3 id="constraints-rules-the-data-cannot-break">Constraints: rules the data cannot break<a class="heading-anchor" href="#constraints-rules-the-data-cannot-break" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The last thing a database gives you is the ability to state what must always be true, and to have those rules enforced no matter which piece of code is doing the writing. A column can be declared unique, so two accounts can never share an email address. A foreign key can require that every order points at a user that actually exists, so deleting a user either cleans up their orders or is refused outright. A <code>NOT NULL</code> says this field is never allowed to be missing.</p>
<p>This matters more than it first appears, because your application is not the only thing that will ever touch this data. There will be a migration script, an admin panel, a background job, a colleague fixing something by hand at eleven at night. Validation in your application protects one path in, and the same reasoning that explains <a href="/why-a-server-can-never-trust-your-browser/">why a server can never trust your browser</a> applies one layer deeper: the database should not fully trust the application either. Constraints are the last line, and they hold for every path.</p>
<h2 id="under-the-hood-it-is-still-files">Under the hood, it is still files<a class="heading-anchor" href="#under-the-hood-it-is-still-files" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Worth deflating one thing before moving on, because it makes the rest easier to think about. A database is a program storing your data in files on an ordinary filesystem. There is no exotic storage layer underneath. Open a Postgres data directory and you find files, and a SQLite database is one single file you can copy with <code>cp</code>.</p>
<p>So the difference between a database and your <code>users.json</code> is not where the bytes live. It is the discipline with which they are written, and it comes down to three ideas. The file is divided into fixed-size blocks called <strong>pages</strong>, so changing one row rewrites one small block instead of everything. Changes are appended to a <strong>write-ahead log</strong> and confirmed on disk before the real pages are touched, which is what makes a commit both durable and fast. And a sorted structure called an <strong>index</strong> is kept alongside the data, so a lookup never has to read all of it.</p>
<p>That layer explains more than it first appears, including why adding an index slows down your inserts and why a database that suddenly feels slow has usually outgrown its memory rather than developed a query problem. <a href="/how-a-database-stores-data-on-disk/">How a database stores data on disk</a> goes through all of it properly.</p>
<h2 id="what-are-the-different-types-of-databases">What are the different types of databases?<a class="heading-anchor" href="#what-are-the-different-types-of-databases" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Everything so far describes the shape most people mean by "a database". It is not the only shape, and the different types of databases exist because they make different trades against the same set of problems.</p>
<p>What separates one database type from another is the <strong>database structure</strong>, meaning how records are organised and how relationships between them are expressed. That single decision determines which questions the engine can answer quickly and which ones it has to work hard for. You will also see these described as types of database management system, since strictly it is the software that differs rather than the data sitting inside it.</p>
<p>Four types cover almost everything you will meet in practice, followed by a group of specialists built for one access pattern each.</p>
<table>
<thead>
<tr>
<th scope="col">Database type</th>
<th scope="col">How the data is structured</th>
<th scope="col">Best at</th>
<th scope="col">Examples</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Relational</th>
<td>Tables of rows and columns, with a fixed schema and keys linking tables</td>
<td>Enforced correctness, complex queries across related data</td>
<td>PostgreSQL, MySQL, SQLite</td>
</tr>
<tr>
<th scope="row">Document</th>
<td>Self-contained nested documents, usually JSON, with no required schema</td>
<td>Irregular shapes, reading a whole record in one lookup</td>
<td>MongoDB, CouchDB</td>
</tr>
<tr>
<th scope="row">Key-value</th>
<td>One key pointing at one value, usually held in memory</td>
<td>Speed, caching, sessions, counters</td>
<td>Redis, Memcached</td>
</tr>
<tr>
<th scope="row">Wide-column</th>
<td>Rows grouped by partition key across many machines</td>
<td>Very high write volume at large scale</td>
<td>Cassandra, HBase</td>
</tr>
<tr>
<th scope="row">Specialists</th>
<td>Whatever suits one access pattern</td>
<td>Time series, search, graphs, analytics</td>
<td>InfluxDB, Elasticsearch, Neo4j, ClickHouse</td>
</tr>
</tbody>
</table>
<h3 id="relational-databases">Relational databases<a class="heading-anchor" href="#relational-databases" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>In a <strong>relational database</strong>, data lives in tables. A table is a fixed set of columns with declared types, and each row is one record. Relationships are expressed by storing a reference to another table's key rather than nesting the data inside.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>     users                         orders</span></span>
<span class="line"><span> ┌──────────────┐            ┌────────────────┐</span></span>
<span class="line"><span> │ id           │◄───────────│ user_id        │</span></span>
<span class="line"><span> │ email        │            │ id             │</span></span>
<span class="line"><span> │ created_at   │            │ total_cents    │</span></span>
<span class="line"><span> └──────────────┘            └────────────────┘</span></span>
<span class="line"><span>   one user                    many orders</span></span></code></pre></div>
<p>A user's email address lives in exactly one place, in the row that owns it, so correcting a typo is a single write no matter how many orders point at that user. That principle is called normalisation, and it trades a little query complexity, since you now have to join tables back together, for the guarantee that no fact is stored twice and able to disagree with itself.</p>
<p>The rigidity is doing real work. Because the engine knows every column and type in advance, it can enforce constraints, plan queries intelligently, and refuse writes that would corrupt the shape of your data. Postgres, MySQL, SQLite, SQL Server and Oracle all sit here, and they are what people mean by <strong>SQL databases</strong>, since they share a query language with the same core in each of them.</p>
<p>The price is that the shape has to be decided up front and changed deliberately. Adding a field means a migration, and if the thing you are storing genuinely has no stable shape, you spend your life fighting the schema.</p>
<h3 id="document-databases">Document databases<a class="heading-anchor" href="#document-databases" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>document database</strong> stores records as self-contained documents, in practice JSON, with no required schema. MongoDB is the common example.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>  relational                  document</span></span>
<span class="line"><span>  ┌──────────┐                {</span></span>
<span class="line"><span>  │ users    │                  "email": "ana@x.com",</span></span>
<span class="line"><span>  ├──────────┤                  "orders": [</span></span>
<span class="line"><span>  │ orders   │                    { "total": 4200 },</span></span>
<span class="line"><span>  ├──────────┤                    { "total": 990 }</span></span>
<span class="line"><span>  │ items    │                  ]</span></span>
<span class="line"><span>  └──────────┘                }</span></span>
<span class="line"><span>  joined at read time         stored together</span></span></code></pre></div>
<p>The pitch is that a record you read together is stored together, so fetching a user with their orders is one lookup rather than a join, and two documents in the same collection can have completely different fields. That suits data with a genuinely irregular shape, like product catalogues where every category has different attributes, or event payloads from many sources.</p>
<p>What you give up is the engine's ability to enforce anything about that shape. Nothing stops half your documents spelling a field <code>emailAddress</code> and the other half <code>email</code>, so the validation has to live in application code, and the rules only hold for the paths that go through it. Duplication comes back too: if a product name is embedded in ten thousand orders, changing it is ten thousand writes. Modern MongoDB does support multi-document transactions, which is a real change from the version most opinions were formed on, but the design still pushes you toward keeping related data in one document rather than spreading it.</p>
<h3 id="key-value-stores">Key-value stores<a class="heading-anchor" href="#key-value-stores" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>key-value store</strong> like Redis does one thing: give it a key, it gives you back a value, extremely fast. There is no query language, no joining and usually no schema, and the data typically lives in memory rather than on disk.</p>
<p>That narrowness is the feature. Redis answers in microseconds, which makes it the standard choice for caching, rate limiting, queues and session storage. It is rarely the place your actual data lives, because memory is expensive and durability is optional. Think of it as a very fast layer sitting in front of a database rather than a replacement for one.</p>
<h3 id="wide-column-stores">Wide-column stores<a class="heading-anchor" href="#wide-column-stores" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>wide-column store</strong> like Cassandra looks superficially like a table, but it is built around spreading data across many machines from the start. Rows are grouped by a partition key that decides which machine holds them, and queries that follow that key are fast while queries that ignore it are difficult or impossible.</p>
<p>That constraint is the trade. You design the structure around the queries you already know you need, rather than storing the data neutrally and deciding later, and in exchange you get write throughput and resilience that a single primary machine cannot match. It is the right answer at a scale most applications never reach, and an awkward one below that.</p>
<h3 id="the-specialists">The specialists<a class="heading-anchor" href="#the-specialists" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Past those four, engines get built for one access pattern that the general-purpose ones handle poorly. Time series databases like InfluxDB or TimescaleDB assume data arrives in timestamp order and is queried in ranges, which lets them compress enormously. Search engines like Elasticsearch build inverted indexes so full-text queries rank results instead of matching them exactly. Graph databases like Neo4j store relationships as first-class objects, so "friends of friends who live in this city" is a traversal rather than a pile of self-joins. Columnar warehouses like ClickHouse or BigQuery store data by column instead of by row, which is what makes analytical queries over billions of rows practical.</p>
<p>The pattern is always the same. Each one wins by assuming something about how you will ask questions, and each one loses whenever you ask a different kind.</p>
<p><img src="/types-of-databases-compared.webp" alt="Five database types shown as panels, relational tables with joins, a nested JSON document, a key pointing to a value, a time ordered series and a graph of connected nodes, each labelled with the access pattern it is built for."></p>
<h3 id="so-sql-or-nosql">So, SQL or NoSQL?<a class="heading-anchor" href="#so-sql-or-nosql" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Worth saying plainly, because the internet is loud about this and the decision is calmer than the arguments. "NoSQL" is not a category so much as a label for everything that is not relational, which is why comparing it as one thing rarely helps.</p>
<p>A relational database is the safer default for most applications. You get transactions, enforced constraints and a query planner without having to give anything up, and Postgres has had JSON columns for years, so the "my data is unstructured" case is covered inside the relational model. The honest test is whether you can name in one sentence the specific property you need that a relational engine handles badly, such as a genuinely unpredictable document shape, full-text ranking, or a write volume beyond what one primary machine can absorb. If you cannot name it, you are choosing based on the argument rather than the problem.</p>
<p>Most real systems end up with more than one anyway: a relational database holding the facts, Redis in front of it for caching, perhaps a search index alongside. That is not indecision, it is each store doing the job it is shaped for.</p>
<h2 id="what-is-a-database-server">What is a database server?<a class="heading-anchor" href="#what-is-a-database-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A <strong>database server</strong> is the program that owns the data and answers queries about it, listening on a port and waiting for connections in the same way any other server does. Postgres listening on 5432 is a database server. The word is also used for the machine that program runs on, which is the same double meaning the word server carries everywhere.</p>
<p>It is one of the <a href="/what-is-a-server-really/">four jobs behind a typical website</a>, and on a small project it runs on the same machine as everything else, as one more process alongside your application.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>   Your application  :3000</span></span>
<span class="line"><span>          ↓  SQL over a connection</span></span>
<span class="line"><span>   Database server   :5432</span></span>
<span class="line"><span>          ↓</span></span>
<span class="line"><span>   Files on disk</span></span></code></pre></div>
<p>When it moves to its own machine, nothing about the model changes, only the distance. A query that was a local socket call becomes a network round trip that can be slow, time out, or fail entirely, and your application has to have an opinion about what to do when it does. Connection pools exist because opening that connection is expensive enough that you want to keep a handful open and reuse them.</p>
<p>That same distance is why so much of a request's time is spent waiting rather than computing, and why runtimes built around <a href="/why-we-needed-node-js/">not blocking while waiting for I/O</a> were such a good fit for web workloads. Your server is rarely busy. It is usually waiting for the database to come back.</p>
<p>Who is allowed to talk to it is an architectural decision with real consequences. One shared database that every service reads and writes is simple and consistent, but it couples everything to one schema, and changing a column becomes a negotiation between teams. Giving each service its own is the other end of the same trade-off at the centre of <a href="/monolith-vs-microservices-explained/">monoliths versus microservices</a>, and it buys independence at the cost of never being able to join across the boundary again.</p>
<p><img src="/shared-database-versus-database-per-service.webp" alt="A comparison of one shared database used by several services on one side, and each service owning its own database on the other, with a crossed out join line between the separate databases."></p>
<h2 id="what-developers-usually-get-wrong">What developers usually get wrong<a class="heading-anchor" href="#what-developers-usually-get-wrong" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The first misconception is that the database is a passive bucket, so all the logic should live in the application. In practice the engine knows things your code cannot: which index exists, how many rows match, what other transactions are currently doing. Pulling ten thousand rows into memory to filter them in Javascript is slower and less correct than asking for the ones you want.</p>
<p>The second is treating an ORM as a replacement for understanding the database. An ORM maps rows to objects, which is genuinely useful, and it also makes it effortless to write a loop that issues one query per iteration without noticing. Knowing what SQL your code is producing is not an advanced skill, it is the baseline for using an ORM well.</p>
<p>The third is assuming the database is where everything belongs. It is the right home for facts you must not lose, and a poor home for things like large files or a stream of events that nobody will read twice. Sessions are the interesting middle case, because keeping them server-side means the database is consulted on every single request, which is one of the trade-offs behind <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">how websites keep you logged in</a>.</p>
<p>And the fourth, which is less a misconception than a warning: what you store is a liability as much as an asset. The reason <a href="/why-websites-cant-tell-you-your-password/">websites cannot tell you your own password</a> is that the sensible thing to keep in a users table is a hash and never the original, and the same instinct applies to everything else you are tempted to save because it might be useful one day.</p>
<h2 id="wrapping-up">Wrapping up<a class="heading-anchor" href="#wrapping-up" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A database is not a spreadsheet with a login. It is the piece of your system that has agreed to be careful, and the guarantees it makes are the ones you would eventually have to build yourself, badly, if it did not exist.</p>
<p>Every feature traces back to a failure in the file version. Transactions exist because a crash can land between two writes. Isolation exists because two requests can arrive at the same instant. Indexes exist because scanning everything to find one row stops working at exactly the moment you start succeeding. Constraints exist because your application will not be the only thing writing to this data.</p>
<p>That is also the useful way to keep learning about them. When something in a database seems arbitrarily complicated, it is almost always the scar tissue from a specific way that data goes wrong, and finding the failure it was built for makes the design obvious in a way no amount of documentation does.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>A database is more than storage. It is a set of guarantees about correctness while many things read and write at the same time.</li>
<li>The database is the data, the DBMS is the software that owns it, and the word gets used for both in conversation.</li>
<li>Plain files break in four predictable ways: concurrent writes silently overwrite each other, crashes leave half-finished state, finding one record means reading all of them, and relationships have nowhere to live.</li>
<li>Transactions make several operations happen as one indivisible step, so there is no observable state where half of them applied.</li>
<li>Constraints enforce the rules for every path into the data, including migrations, admin tools and manual fixes that never touch your application code.</li>
<li>Underneath, a database is still files on a normal filesystem. What differs is the discipline: fixed-size pages instead of one blob, a write-ahead log appended before anything is changed, and sorted index structures so a lookup never scans everything.</li>
<li>The types of database differ by what they assume about your questions. Relational enforces shape and joins, document trades that enforcement for flexibility, key-value trades everything for speed, and the specialists win on one access pattern each.</li>
<li>The database is a server on a port, and moving it to its own machine turns every query into a network call that can be slow or fail.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="what-is-the-difference-between-a-database-and-a-dbms">What is the difference between a database and a DBMS?<a class="heading-anchor" href="#what-is-the-difference-between-a-database-and-a-dbms" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The database is the stored data itself. The DBMS, or database management system, is the running software that organises it, enforces the rules and answers queries, such as PostgreSQL, MySQL or MongoDB. This is why types of database management system and database types mean the same thing in practice: it is the software that differs, not the data. People say "database" for both, which is only a problem when it is unclear whether the data or the process is the thing that has gone wrong.</p>
<h3 id="is-excel-a-database">Is Excel a database?<a class="heading-anchor" href="#is-excel-a-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not in any useful sense. A spreadsheet stores rows and can filter them, but it has no transactions, no enforced types or relationships, and no way to handle many people writing at once without someone's changes being lost. Those are the specific problems a database exists to solve.</p>
<h3 id="can-i-use-a-json-file-instead-of-a-database">Can I use a JSON file instead of a database?<a class="heading-anchor" href="#can-i-use-a-json-file-instead-of-a-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>You can, and for a single-user script it is a reasonable choice. It stops working as soon as two requests can arrive at the same time, because both read the same version and the second write erases the first, and a crash mid-write can leave the file unreadable with no earlier copy to fall back on.</p>
<h3 id="what-does-acid-mean">What does ACID mean?<a class="heading-anchor" href="#what-does-acid-mean" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>ACID describes four guarantees a transactional database makes: atomicity, so a transaction applies fully or not at all; consistency, so the data still obeys its rules afterwards; isolation, so concurrent transactions do not see each other's unfinished work; and durability, so a committed change survives a crash.</p>
<h3 id="what-are-the-main-types-of-databases">What are the main types of databases?<a class="heading-anchor" href="#what-are-the-main-types-of-databases" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Relational databases store rows in tables with a fixed schema, document databases store schema-free nested documents, key-value stores map one key to one value in memory, and wide-column stores spread table-like data across many machines. Alongside those sit specialists built for one access pattern: time series, full-text search, graphs and columnar analytics.</p>
<h3 id="sql-or-nosql-which-should-i-use">SQL or NoSQL, which should I use?<a class="heading-anchor" href="#sql-or-nosql-which-should-i-use" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>For most applications, a relational database is the safer default, because you get transactions, constraints and a query planner without giving anything up. Reach for something else when you can name the specific property you need that relational engines handle poorly, such as an unpredictable document shape or a write volume beyond what one primary can take.</p>
<h3 id="does-the-database-have-to-run-on-a-separate-machine">Does the database have to run on a separate machine?<a class="heading-anchor" href="#does-the-database-have-to-run-on-a-separate-machine" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. It is a process listening on a port, and running it alongside your application is completely normal for small projects. Separating it becomes worthwhile when you want to scale, back up or restart the two independently, and the cost is that every query becomes a network call.</p>
<h2 id="continue-reading">Continue reading<a class="heading-anchor" href="#continue-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Three articles pick up the threads this one leaves hanging, one on the storage layer underneath, one on the machine it runs on, and one on who is allowed to talk to it.</p>
<ul>
<li><a href="/how-a-database-stores-data-on-disk/">How a database stores data on disk</a>. Pages, the write-ahead log, crash recovery and what an index physically is, which is where the guarantees in this article are actually implemented.</li>
<li><a href="/what-is-a-server-really/">What is a server, really?</a>. The database server is one of four jobs behind a typical website, and this covers the other three and how they fit together.</li>
<li><a href="/monolith-vs-microservices-explained/">Monolith vs microservices explained</a>. What happens to your data when the application splits into services, and why one shared database is the decision that usually decides the rest.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>What is a server? Types, Hardware, Software and the Cloud</title>
      <link>https://themissinglevel.dev/what-is-a-server-really/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/what-is-a-server-really/</guid>
      <pubDate>Tue, 18 Aug 2026 06:00:00 GMT</pubDate>
      <description>What is a server? Learn how servers work, why the word means hardware or software, what the main server types do, and how machines become the cloud.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>You hear the word on your first day and nobody ever stops to define it. The server is down. Push it to the server. Ask the backend team to check the server. Everyone nods, the conversation moves on, and you quietly build a mental picture out of context clues.</p>
<p>For a long time mine was a black box in a cold room somewhere. That picture is not wrong, but it is only one third of the story, and it is the least useful third for a developer.</p>
<p>The reason the word feels vague is that it is doing three jobs at once. Sometimes it means a physical machine. Sometimes it means a program running on that machine. Sometimes it means neither of those and the speaker is talking about a rented slice of someone else's hardware. All three uses are correct, which is exactly why the word slides around in conversation.</p>
<p>So let's take the three apart, one at a time.</p>
<h2 id="what-is-a-server">What is a server?<a class="heading-anchor" href="#what-is-a-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A <strong>server</strong> is something that waits for requests and answers them.</p>
<p>That is the whole definition. Not a kind of hardware, not a brand, not a place. <strong>It is a role</strong> in a conversation. Something asks, something answers, and whichever side does the answering is the server for that exchange.</p>
<p>The other side of that conversation is the <strong>client</strong>. Your browser is a client. So is a mobile app, a <code>curl</code> command, or a payment provider calling your webhook. The naming describes who starts the conversation and who responds to it, nothing more.</p>
<h3 id="a-server-is-not-necessarily-a-separate-computer">A server is not necessarily a separate computer<a class="heading-anchor" href="#a-server-is-not-necessarily-a-separate-computer" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A server does not have to be a special machine sitting in a data centre. It is usually a program running on a computer.</p>
<p>Your laptop can be a server. Your phone can be a server. A program inside a virtual machine can be a server.</p>
<p>What matters is the role the program is playing: it waits for requests and responds to them.</p>
<p>This is why the same machine can be both. Your application server is a server when the browser talks to it, and a client the moment it turns around and queries the database. The role changes depending on which conversation you are looking at.</p>
<p><img src="/client-and-server-are-roles-not-machines.webp" alt="A diagram showing a browser, an application server and a database in a row, where the application server is the server when the browser asks it for something and the client when it asks the database, showing that client and server are roles rather than machines." style="max-width:90%" class="img-narrow"></p>
<p>The practical consequence is that there is nothing special about the machine. The laptop you are reading this on can be a server in about ten seconds, and later in this article we will do exactly that. What makes something a server is that a program on it is listening, and that it stays running when nobody is using it.</p>
<p>That last part matters more than people expect. A client can make a request and disappear. A server needs to be available when the request arrives, which is where all the boring requirements come from: uptime, restarts, monitoring, and reliable infrastructure.</p>
<h2 id="is-a-server-hardware-or-software">Is a server hardware or software?<a class="heading-anchor" href="#is-a-server-hardware-or-software" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Now the confusing part. When someone says "the server", they might mean the metal or they might mean the program, and the sentence usually does not tell you which.</p>
<p>When server refers to hardware, it usually means a computer designed to run services reliably and continuously, often with features that make it easier to operate and maintain remotely. No monitor, no speakers, no concern for looking nice. Instead it has redundant power supplies so one failure does not take it offline, error-correcting memory that catches bit flips instead of quietly corrupting data, drives designed to spin for years, and a flat shape so it slides into a rack with dozens of others. It is optimised to run unattended for a very long time.</p>
<p>The software meaning is a program that listens on a port and answers requests. Nginx is a server. Postgres is a server. The Node process running your API is a server. None of them are objects you could drop on your foot.</p>
<p><img src="/server-hardware-versus-server-software.webp" alt="A comparison of the two meanings of the word server, with a flat rack-mounted machine with redundant power supplies and error-correcting memory on one side, and a single computer running Nginx, an application, Postgres and SSH as four separate listening programs on the other." style="max-width:80%" class="img-narrow"></p>
<p>One physical machine usually runs several of these at once:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>        One physical machine</span></span>
<span class="line"><span> ┌───────────────────────────────┐</span></span>
<span class="line"><span> │  Nginx        listening :443  │</span></span>
<span class="line"><span> │  Your app     listening :3000 │</span></span>
<span class="line"><span> │  Postgres     listening :5432 │</span></span>
<span class="line"><span> │  SSH daemon   listening :22   │</span></span>
<span class="line"><span> └───────────────────────────────┘</span></span>
<span class="line"><span>     four servers, one computer</span></span></code></pre></div>
<p>This is why "the server is down" is such an unhelpful sentence. The machine could be off, or the machine could be perfectly healthy while one process on it crashed. Those are completely different problems with completely different fixes, and the word covers both.</p>
<p>Worth knowing the habit: when a developer says server they almost always mean the software, and when someone from operations says it they usually mean the machine.</p>
<h2 id="how-does-a-server-handle-a-request">How does a server handle a request?<a class="heading-anchor" href="#how-does-a-server-handle-a-request" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Underneath every server, whatever it is written in, sits the same loop.</p>
<p>It opens a port and waits. A connection arrives. It reads the request, works out what is being asked, does whatever work that requires, writes a response back, and goes straight back to waiting. That is the entire lifecycle, repeated a few thousand times a second on a busy day.</p>
<p>Here is the loop as actual code, which is the fastest way to prove that a server is not a mysterious thing:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> express </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> "express"</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> app</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> express</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">app.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/"</span><span style="color:#E1E4E8">, (</span><span style="color:#FFAB70">request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">response</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  response.</span><span style="color:#B392F0">send</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"Hello from a server"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">app.</span><span style="color:#B392F0">listen</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">3000</span><span style="color:#E1E4E8">);</span></span></code></pre></div>
<p>A handful of lines, and your laptop is now a server. It is listening on port 3000, and anything that can reach that port can ask it for a response. The fact that this is possible in Javascript at all is a <a href="/why-we-needed-node-js/">fairly recent development</a>.</p>
<p>In HTTP/1.1, the request is transmitted as a text-based message. When a browser talks to a web server, it sends a request describing what it wants, followed by <a href="/seven-http-request-headers-every-developer-should-understand/">headers that describe who is asking and what they can accept</a>. The server replies with a status code, its own headers, and the body. <a href="/what-happens-after-you-press-enter/">You can follow that whole journey from the address bar to the pixels here</a>.</p>
<p>Two properties of this loop shape almost everything else.</p>
<p>The first is that HTTP is stateless. The connection underneath may be reused for several requests, but the server does not automatically retain application state from one request to the next. Unless you build something on top, it has no way to know that the request it is handling came from the same person who logged in a minute ago. Everything about <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">staying logged in</a> exists to work around this.</p>
<p>The second is that requests do not politely queue up one at a time. Hundreds can be in flight at once, and how a server handles that overlap is the main thing separating one server technology from another.</p>
<h2 id="what-are-the-main-types-of-servers">What are the main types of servers?<a class="heading-anchor" href="#what-are-the-main-types-of-servers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Here is the thing that took me embarrassingly long to notice. When people say web server, database server or file server, they are not describing four different kinds of computer. They are describing four different jobs, and the same box can do all of them at once.</p>
<p>The names tell you what the software does, not what the hardware is.</p>
<p><img src="/the-four-main-types-of-servers.webp" alt="The four main types of servers shown as four panels, a web server handling HTTP, TLS and static files, an application server running your code, a database server owning the data, and a file server storing and sharing files, under the caption four jobs not four machines."></p>
<h3 id="what-is-a-web-server">What is a web server?<a class="heading-anchor" href="#what-is-a-web-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>web server</strong> speaks HTTP. Its job is to accept connections from the outside world and deal with the parts of a request that have nothing to do with your business logic.</p>
<p>MDN describes the same split in its own introduction to <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_web_server">what a web server is</a>, hardware on one side and software on the other, which is a good sign that the confusion is not just you.</p>
<p>Nginx and Apache are some of the most common ones you will meet. They serve static files straight off disk, handle the TLS handshake so your traffic is encrypted, compress responses, and pass anything dynamic along to whatever actually generates it. That last job is called reverse proxying, and it is why a web server usually sits in front of your application rather than containing it.</p>
<p>If a request is for <code>/logo.png</code>, the web server answers it alone and your code never runs. If the request is for <code>/checkout</code>, it hands it onward.</p>
<h3 id="what-is-an-application-server">What is an application server?<a class="heading-anchor" href="#what-is-an-application-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>An <strong>application server</strong> runs your code. This is the layer where a URL becomes a decision: check who is asking, read from the database, apply the rules, build a response.</p>
<p>Your Express API, your Rails app, your Django project and your Spring service all sit here. The distinction from a web server used to be sharp, because the thing running your code was not built to face the open internet and needed something hardened in front of it. Modern runtimes blur it, since a Node process can happily terminate TLS and serve static files itself. Most production setups still keep the two separate, because the boring layer is very good at the boring work and there is no reason to make your application do it.</p>
<h3 id="what-does-a-database-server-do">What does a database server do?<a class="heading-anchor" href="#what-does-a-database-server-do" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>database server</strong> owns the data and answers questions about it. Postgres, MySQL, MongoDB and Redis are all servers by the definition we started with, they just do not speak HTTP.</p>
<p>Postgres listens on port 5432 and speaks its own protocol, so you talk to it with a client library rather than a browser. It handles <a href="/what-is-a-database/">the things you would not want to write yourself</a>: concurrent writes without corruption, transactions that either fully happen or fully do not, <a href="/how-a-database-stores-data-on-disk/">indexes that turn a full scan into an instant lookup</a>, and rules about what happens when two people edit the same row at the same moment.</p>
<p>One detail matters more than the rest. A database server should not be reachable from the internet. Only your application should be able to open a connection to it, because a database exposed to the world is the shortest path between a small misconfiguration and a very bad week.</p>
<h3 id="what-is-a-file-server">What is a file server?<a class="heading-anchor" href="#what-is-a-file-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>file server</strong> stores files and hands them out over a network. Inside a company this is the shared drive that appears on everyone's machine, usually running SMB on Windows networks or NFS on Unix ones, so the files feel local while living somewhere else entirely.</p>
<p>One protocol commonly associated with file servers is <strong>FTP</strong>, the File Transfer Protocol. For years, it was a common way to upload a website to a host: connect, drag your folder across, done. Today, encrypted alternatives such as SFTP are generally preferred when transferring files over untrusted networks.</p>
<p>The modern version of this job usually is not a file server at all. Object storage like S3 does the same thing over HTTP, with the network drive metaphor dropped completely.</p>
<h2 id="how-do-web-application-and-database-servers-work-together">How do web, application and database servers work together?<a class="heading-anchor" href="#how-do-web-application-and-database-servers-work-together" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Put the main application layers in a line and a normal request looks like this:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>   Browser</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span>  Web server        TLS, static files, routing</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span> App server         your code, your rules</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span> Database server    the data itself</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span>   Response back up the same path</span></span></code></pre></div>
<p>Each layer does one job and hands the rest along. That is the shape behind most of the web, whether the thing serving you is a personal blog or a bank.</p>
<p>What throws people is that this diagram says nothing about how many computers are involved. On a small site all four layers run on one rented machine that costs a few euros a month, and that is a completely legitimate way to run a real application. On a large one, each layer is a separate cluster and there are load balancers, caches and queues in the gaps.</p>
<p>The layers stayed the same. Only the machine count changed.</p>
<p>Splitting them apart buys you the ability to scale each piece independently and to fail in smaller pieces, and it costs you the fact that a function call becomes a network call that can time out. That trade is the same one at the centre of <a href="/monolith-vs-microservices-explained/">monoliths versus microservices</a>, one level further up.</p>
<p><img src="/same-server-layers-one-machine-or-four.webp" alt="A comparison showing a web server, an application server, a database and file storage running together inside one machine on the left, and the same four split across four separate machines on the right, with a request following the same path through both."></p>
<h3 id="an-example-vite-and-express">An example: Vite and Express<a class="heading-anchor" href="#an-example-vite-and-express" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>If the layers still feel abstract, here is a stack a lot of people actually run, and the confusion that comes with it. Both answers feel correct depending on when you look.</p>
<p>In <strong>development</strong>, you are running two servers at once. Vite serves your frontend on port <code>5173</code>, keeps a connection open so the page updates the moment you save a file, and forwards anything starting with <code>/api</code> to Express on port <code>3000</code>. Express runs your routes and talks to the database.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>   Browser</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span>  Vite dev server  :5173    frontend files, hot reload</span></span>
<span class="line"><span>      ↓                     /api → :3000</span></span>
<span class="line"><span>  Express          :3000    your code, your rules</span></span></code></pre></div>
<p>The important bit is that the browser only knows about port <code>5173</code>. If your frontend calls <code>/api/products</code>, the browser sends that request to the same origin it got the page from.</p>
<p>Vite receives it and, because of the proxy setting in your Vite config, forwards the request to Express.</p>
<p>So Express is handling the API request, but the browser never talks to port <code>3000</code> directly. This is useful in development because your frontend can call <code>/api/products</code> without having to know where the API server is running, and Vite can take care of forwarding it.</p>
<p>Two processes, two jobs, one browser-facing port.</p>
<p>Then in <strong>production</strong>, Vite is not there any more. This surprised me the first time I looked for it. Running vite build happens once, writes finished files into a dist folder, and exits. Vite is a build tool that happens to include a dev server for your convenience, not something your application normally runs in production.</p>
<p>So in production the question becomes: who serves that dist folder, and who receives the API requests?</p>
<p>There are a couple of perfectly normal answers.</p>
<p><img src="/express-alone-or-nginx-in-front.webp" alt="Comparison of Express serving the frontend and API directly versus Nginx serving the frontend and proxying API requests to Express." style="max-width:90%" class="img-narrow"></p>
<p>In the first setup, Express is doing both jobs in a single process. It serves the finished frontend files and runs the API.</p>
<p>In the second, Nginx sits in front. It serves the static files itself and forwards only /api requests to Express:</p>
<p>So why use Nginx at all if Express can already serve the files?</p>
<p>Because serving a few files is easy. Handling the boring infrastructure around those files is something Nginx is very good at. It can terminate HTTPS, serve static files efficiently, compress responses, add caching rules, handle connections, and act as a reverse proxy in front of one or several application processes.</p>
<p>That separation also means Express can concentrate on your application: authentication, business rules, database queries and API responses. Nginx handles the traffic coming in from the outside world.</p>
<p>You do not always need it. A small application can perfectly well have Express serve the frontend and API itself. Nginx becomes useful when you want to separate those responsibilities or need the extra infrastructure features around your application.</p>
<p>And this is the whole point again: these are jobs, not products. Express can perform the application-server job and, in some setups, the web-server job too. Nginx can perform the web-server and reverse-proxy jobs. Asking which one is "the web server" only has an answer once you say which setup, and which moment, you are talking about.</p>
<h2 id="other-types-of-servers-proxy-dns-vps-and-mcp">Other types of servers: proxy, DNS, VPS and MCP<a class="heading-anchor" href="#other-types-of-servers-proxy-dns-vps-and-mcp" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Four types cover the stack behind a website, but the naming pattern keeps going, and the same rule applies every time. The word in front tells you the job.</p>
<p>A <strong>proxy server</strong> sits between a client and a server and passes traffic along, either to protect the client's identity on the way out or to shield and cache for the server on the way in. A <strong>DNS server</strong> answers the question of which IP address a domain name points to, which is a whole journey of its own that <a href="/what-happens-during-a-dns-lookup-a-step-by-step-guide/">a single DNS lookup</a> walks through step by step. A <strong>DHCP server</strong> hands out local IP addresses when a device joins a network, which is why your laptop gets an address the moment it connects to wifi without you configuring anything. A mail server moves email around.</p>
<p>One name in that family breaks the pattern, and it is worth calling out. A <strong>VPS</strong>, or virtual private server, does not describe a job at all. It describes ownership: a slice of a bigger physical machine, rented to you, that behaves like a whole computer you control. What you run on it is entirely your business.</p>
<h3 id="what-is-an-mcp-server">What is an MCP server?<a class="heading-anchor" href="#what-is-an-mcp-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The newest name in the family is the one that confuses people most, because it arrived attached to AI and everyone assumed it must be something exotic. An <strong>MCP server</strong>, from the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a>, is a program that exposes tools and data to an AI model in a format the model can call. Your editor's assistant wants to read a file, query your database or open a pull request, and an MCP server is the thing sitting on the other side of that request, deciding what it is allowed to do and handing back the answer.</p>
<p>Held up against the definition we started with, it fits perfectly. Something waits, something asks, an answer comes back. What changed is only who is doing the asking. For thirty years the client was a browser or another program written by a human, and now it can be a model deciding on its own that it needs the contents of a file. The protocol is new, the shape is not, and that is genuinely the whole trick.</p>
<h3 id="game-servers-media-servers-and-time-servers">Game servers, media servers and time servers<a class="heading-anchor" href="#game-servers-media-servers-and-time-servers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The pattern keeps going well past web development, and the further out you go the clearer it gets that the word is doing the same job every time. A game server holds the authoritative state of a match and tells every connected player what actually happened, which is the same trust problem a web application has, at sixty updates a second. A media server sits on a machine at home and streams your own files to whatever device asks for them. A time server answers one very small question, what time is it, and it matters more than it sounds, because certificates, logs and distributed systems all fall apart quickly when two machines disagree about the clock.</p>
<p>Different protocols, different ports, different problems. Same loop underneath.</p>
<h2 id="where-do-servers-live-data-centres-server-racks-and-the-cloud">Where do servers live? Data centres, server racks and the cloud<a class="heading-anchor" href="#where-do-servers-live-data-centres-server-racks-and-the-cloud" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The hardware meaning has to physically be somewhere, and that somewhere is almost always a data centre.</p>
<p>Inside one, machines are mounted in a <strong>server rack</strong>, a metal frame a bit under two metres tall holding equipment in standard slots. Each slot is a rack unit, or 1U, about 4.4 centimetres high, which is why servers are sold as 1U or 2U and why they are so unnaturally flat. Stacking them this way means one rack can hold dozens of machines while keeping cabling manageable and letting cold air flow through the front and out the back.</p>
<p>The building around the racks is the actual product. Redundant power feeds with battery backup and generators, cooling that runs constantly because a rack of machines produces a serious amount of heat, multiple independent network connections, and physical security. Very few companies want to own any of that, which is where the cloud comes in.</p>
<p>"The cloud" is not a technology, it is an arrangement. Someone else buys the racks, staffs the building, replaces the failed drives, and rents you the use of the machines by the hour. Virtualisation is what makes it work: one physical machine is divided into many isolated virtual ones, so you get a computer that behaves like it is yours while sharing hardware with strangers.</p>
<p>Nothing about the definition changed. Your code still runs on a specific machine, in a specific building, in a specific country. The only thing that changed is whose name is on the invoice for the hardware, and how quickly you can get another one.</p>
<p><img src="/server-rack-and-what-the-cloud-rents-you.webp" alt="A server rack holding eight flat stacked machines with a single one unit slot highlighted, and an arrow from that slot across to a box labelled your virtual server, showing that the cloud means renting a slice of the hardware rather than owning the rack." style="max-width:80%" class="img-narrow"></p>
<h2 id="wrapping-up">Wrapping up<a class="heading-anchor" href="#wrapping-up" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The word stops being vague once you know it is carrying three meanings. A machine that stays on, a program that listens, and a rented slice of someone else's hardware. When a sentence confuses you, it is almost always because two people in the room picked different meanings.</p>
<p>Underneath all three is the same small idea. Something is waiting, something asks, an answer comes back. Every type in this article is a variation on that one loop, differing only in what it listens on and what it does before it replies.</p>
<p>Keeping the roles straight also changes how you think about safety. Once you can see that the client and the server are two separate computers owned by two separate people, it becomes obvious <a href="/why-a-server-can-never-trust-your-browser/">why a server can never trust your browser</a>, and a large part of web security follows from there.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>A server is a role, not a kind of machine. Whatever answers a request is the server for that exchange, and the same computer can be a server in one conversation and a client in the next.</li>
<li>The word carries a hardware meaning and a software meaning at the same time, which is why "the server is down" can describe two completely different problems.</li>
<li>Every server runs the same loop: listen on a port, read a request, do the work, write a response, wait again.</li>
<li>Web, application, database and file servers are four jobs rather than four machines, and on a small site all four run on one computer.</li>
<li>The cloud does not remove the physical machine. It changes who owns the rack and how fast you can rent another slot in it.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="is-a-server-just-a-normal-computer">Is a server just a normal computer?<a class="heading-anchor" href="#is-a-server-just-a-normal-computer" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Mostly yes, with different priorities. Server hardware drops the things a desktop needs, like a monitor and a sound card, and adds redundant power supplies, error-correcting memory and a flat shape that fits a rack. Any ordinary computer can act as a server the moment it runs a program that listens for requests.</p>
<h3 id="what-is-the-difference-between-a-web-server-and-an-application-server">What is the difference between a web server and an application server?<a class="heading-anchor" href="#what-is-the-difference-between-a-web-server-and-an-application-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A web server handles HTTP itself: encryption, static files, compression, and passing dynamic requests onward. An application server runs your code and produces the answers. In production they are usually separate processes, with the web server sitting in front, though modern runtimes can do both jobs in one.</p>
<h3 id="can-my-laptop-be-a-server">Can my laptop be a server?<a class="heading-anchor" href="#can-my-laptop-be-a-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes, and it already is one whenever you run a dev server locally. The difference between that and production is not the hardware, it is that a production server has a public address, stays on permanently, and is expected to survive restarts and failures without anyone watching.</p>
<h3 id="what-is-a-server-rack">What is a server rack?<a class="heading-anchor" href="#what-is-a-server-rack" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A server rack is a standard metal frame that holds servers and network equipment in stacked slots. Each slot is one rack unit, or 1U, about 4.4 centimetres high, which is why servers are described as 1U or 2U. Racks keep dozens of machines in a small footprint with predictable cabling and airflow.</p>
<h3 id="do-i-need-my-own-server-to-host-a-website">Do I need my own server to host a website?<a class="heading-anchor" href="#do-i-need-my-own-server-to-host-a-website" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Managed hosting, static hosting and serverless platforms all run your site on servers that someone else operates and configures for you. There is still a server answering every request, you are simply not the one maintaining it.</p>
<h2 id="continue-reading">Continue reading<a class="heading-anchor" href="#continue-reading" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Two articles pick up directly where this one stops, one on the security side and one on the architecture side.</p>
<ul>
<li><a href="/why-a-server-can-never-trust-your-browser/">Why a server can never trust your browser</a>. Now that the client and the server are two clearly separate machines, this is what follows from it, and it is where most web security starts.</li>
<li><a href="/monolith-vs-microservices-explained/">Monolith vs microservices explained</a>. What actually happens when you take the layers from this article and split them across separate machines, and what that costs you.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why a server can never trust your browser</title>
      <link>https://themissinglevel.dev/why-a-server-can-never-trust-your-browser/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-a-server-can-never-trust-your-browser/</guid>
      <pubDate>Thu, 13 Aug 2026 21:00:00 GMT</pubDate>
      <description>Every check you write in the browser can be edited away. Learn where the trust boundary sits and why client-side validation is convenience, not security.</description>
      <category>Security</category>
      <content:encoded><![CDATA[<p>There’s a moment in most developers’ careers when someone reviews your pull request and leaves a comment that feels almost insulting.</p>
<p>You’ve already validated the form. The email field checks for an <code>@</code>, the quantity field rejects negative numbers, and the submit button stays disabled until everything is filled in.</p>
<p>Then the reviewer writes:</p>
<blockquote>
<p>This needs to happen on the server too.</p>
</blockquote>
<p>First time I saw it, my reaction was that they hadn’t read the code properly. The validation was right there. Why would we write the same thing twice? It felt like the kind of ceremony that exists because a senior developer got burned by it once, and now everyone else has to pay the tax forever.</p>
<p>It took me longer than I’d like to admit to understand that these two checks aren’t duplicates at all. They happen in different places, for different reasons, and they protect you from very different problems.</p>
<p>One is there to help your users. The other is there because <strong>the browser belongs to the user</strong>, and the user can change whatever they want.</p>
<p>That small idea changes the way you look at a lot of web development.</p>
<p>Once you understand why a server can’t trust your browser, things like server-side validation, authentication, authorization, and even seemingly harmless form checks start to make a lot more sense.</p>
<h2 id="what-is-the-client-server-trust-boundary">What is the client-server trust boundary?<a class="heading-anchor" href="#what-is-the-client-server-trust-boundary" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Let's say you have created and deployed an online shop.</p>
<p>When someone opens their browser and <a href="/what-happens-after-you-press-enter/">presses Enter in their address bar</a>, a lot of machinery wakes up. Their browser <a href="/what-happens-during-a-dns-lookup-a-step-by-step-guide/">resolves the domain</a>, opens a connection, and sends a request.</p>
<p>Your server sends back the HTML, CSS, and <a href="/why-we-needed-javascript/">Javascript</a> that make up your website. That code now runs in the visitor's browser, <strong>on their machine</strong>.</p>
<p>This is where the important distinction begins: <strong>the browser belongs to the visitor; the server belongs to you</strong>.</p>
<p>The visitor can inspect your Javascript, change it, open DevTools, disable things, or send requests directly to your server.</p>
<p>You control what happens on <a href="/what-is-a-server-really/">the server</a>.</p>
<p>That creates the trust boundary:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>      Their machine                     Your server</span></span>
<span class="line"><span>  ┌─────────────────────┐         ┌─────────────────────┐</span></span>
<span class="line"><span>  │ Browser             │         │ Server              │</span></span>
<span class="line"><span>  │ HTML, CSS, JS       │         │ App code, database  │</span></span>
<span class="line"><span>  │ DevTools, extensions│         │ Nobody else's hands │</span></span>
<span class="line"><span>  │                     │         │                     │</span></span>
<span class="line"><span>  │ They control this   │         │ You control this    │</span></span>
<span class="line"><span>  └─────────────────────┘         └─────────────────────┘</span></span>
<span class="line"><span>             │                               │</span></span>
<span class="line"><span>             └──────────  HTTP  ─────────────┘</span></span>
<span class="line"><span>                    the trust boundary</span></span></code></pre></div>
<p>Anything on the left side is a claim. Anything on the right side is something your application can verify and enforce.</p>
<p>A lot of web security comes down to remembering this boundary, and the expensive lessons that happen when we forget it.</p>
<h2 id="why-client-side-validation-can-be-bypassed-in-seconds">Why client-side validation can be bypassed in seconds<a class="heading-anchor" href="#why-client-side-validation-can-be-bypassed-in-seconds" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Imagine now that you’re building a checkout feature for your online shop. The main scenario you have in mind is a customer finding a product and deciding to buy it.</p>
<p>For our example, let’s say the product costs €50.</p>
<p>Your checkout page sends the price in a hidden form field:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">input</span><span style="color:#B392F0"> type</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"hidden"</span><span style="color:#B392F0"> name</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"price"</span><span style="color:#B392F0"> value</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"50"</span><span style="color:#E1E4E8">></span></span></code></pre></div>
<p>You also have some Javascript that checks the form before allowing the customer to submit it. You test everything, place a few orders, and everything works. Customers pay the correct price.</p>
<p>The problem is that once your site is in production, the browser is running on the customer's machine. The customer can open DevTools and change what your page sent to their browser.</p>
<p>For example, they could change:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">input</span><span style="color:#B392F0"> type</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"hidden"</span><span style="color:#B392F0"> name</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"price"</span><span style="color:#B392F0"> value</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"50"</span><span style="color:#E1E4E8">></span></span></code></pre></div>
<p>to:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">input</span><span style="color:#B392F0"> type</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"hidden"</span><span style="color:#B392F0"> name</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"price"</span><span style="color:#B392F0"> value</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"1"</span><span style="color:#E1E4E8">></span></span></code></pre></div>
<p><img src="/editing-a-hidden-price-field-in-devtools.webp" alt="A checkout form in a browser showing a price of 249 dollars, with the developer tools open underneath revealing the hidden price input edited down to 1, and the tampered request being sent to the server anyway." style="max-width:80%" class="img-narrow"></p>
<p>Now the browser can send 1 as the price.</p>
<p>They could also change or remove a <code>min="1"</code> restriction on the quantity field, remove the Javascript checks, or skip the checkout page completely and send the HTTP request themselves with curl.</p>
<p>This is the important part: <strong>the server cannot trust the client to follow the rules defined in the frontend.</strong></p>
<p>And the client doesn't have to be a browser at all. Someone can simply send the request directly:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>POST /checkout HTTP/1.1</span></span>
<span class="line"><span>Host: yourshop.example</span></span>
<span class="line"><span>Content-Type: application/json</span></span>
<span class="line"><span></span></span>
<span class="line"><span>{ "productId": 4417, "quantity": 1, "price": 1 }</span></span></code></pre></div>
<p>From the server's point of view, this is just a normal HTTP request.</p>
<p>HTTP doesn't tell the server:</p>
<blockquote>
<p>"This request came from your checkout page, and the user didn't change anything."</p>
</blockquote>
<p>The server receives the request and has to decide whether the data is valid.</p>
<p>That's why important values should be checked on the server. Instead of trusting the price sent by the browser, the server should receive the product ID and look up the real price itself.</p>
<p>That’s the part that reframes everything.</p>
<p>Client-side validation isn't weak security. It isn't security at all. It's a user experience feature. It can tell someone their email is missing an <code>@</code> before they have to wait for a round trip, and that’s genuinely valuable.</p>
<p>In the end, though, the server is the one that has the final say.</p>
<p>The browser can help, but it has no authority.</p>
<h2 id="which-parts-of-an-http-request-can-be-spoofed">Which parts of an HTTP request can be spoofed<a class="heading-anchor" href="#which-parts-of-an-http-request-can-be-spoofed" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Before we dive into which parts of a request can be spoofed, let's first
understand what <strong>spoofing</strong> actually means.</p>
<p>In simple terms, spoofing means <strong>pretending to be something you are
not</strong>.</p>
<p>In the context of web requests, it means a client sends information that
makes the request look different from what it really is. The important
detail is that the client gets to choose what goes into the request.</p>
<p>Once you understand the trust boundary, a lot of familiar things start
looking different. A request is made up of many pieces of information,
but the server shouldn't automatically treat all of them as facts.</p>
<p>A useful way to think about this is to group the things a client can
send into a few broad categories.</p>
<h3 id="1-form-fields-and-request-data">1. Form fields and request data<a class="heading-anchor" href="#1-form-fields-and-request-data" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>This is the easiest category to understand because we just saw it with
the checkout example.</p>
<p>A browser might send:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "productId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">4417</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "quantity"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "price"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">50</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
<p>But the client can change any of those values before sending the request:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "productId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">4417</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "quantity"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "price"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
<p>The server therefore cannot trust the price simply because it came from a form your application created.</p>
<p>The same applies to things like:</p>
<ul>
<li>quantity</li>
<li>account IDs</li>
<li>roles</li>
<li>permissions</li>
<li>discount codes</li>
<li>feature flags</li>
<li>hidden form fields</li>
</ul>
<p>If the client sends the value, the client can potentially change it.</p>
<h3 id="2-request-headers">2. Request headers<a class="heading-anchor" href="#2-request-headers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Headers can also look more trustworthy than they really are.</p>
<p>Take <code>User-Agent</code>. A browser normally sends information about itself:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>  User-Agent: Mozilla/5.0 ... Chrome/131.0 ...</span></span></code></pre></div>
<p>Your server might use this for analytics or logging. That's fine. But if you use it as a security check, you're trusting the client to tell
you the truth.</p>
<p>A request made with <code>curl</code> can send the same value:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>curl https://yourshop.example/api/example \</span></span>
<span class="line"><span>-H "User-Agent: Mozilla/5.0 ... Chrome/131.0 ..."</span></span></code></pre></div>
<p>Your server sees the same header.</p>
<p>The same idea applies to headers such as <code>Referer</code>. It can tell you where
the client says the request came from, but it isn't proof that the request
actually came from there. The same caution applies to the rest of the
<a href="/seven-http-request-headers-every-developer-should-understand/">HTTP request headers you'll meet every day</a>,
because every one of them is written by the client.</p>
<h3 id="3-identity-and-authentication-data">3. Identity and authentication data<a class="heading-anchor" href="#3-identity-and-authentication-data" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>This category is a little different. A client can send a cookie containing a session ID:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>  Cookie: sessionId=abc123</span></span></code></pre></div>
<p>The browser is still choosing to send that value. The important question is
what the server does with it.</p>
<p>A secure session system doesn't blindly trust the cookie just because it
came from the browser. Instead, the server uses the session ID to
<a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">look up the session in its own store</a>.</p>
<p>But there is still a small amount of trust involved. The server is
essentially assuming that whoever presents a valid session ID is the person
who should be using that session. It relies on the session ID being kept
secret and not being stolen.</p>
<p>So the browser carries the identifier, but the server decides what that
identifier means.</p>
<p>The same principle applies to signed tokens. A client can read a token and
send it back, but if they modify its contents, the server can detect the
change because the signature no longer matches.</p>
<p>So authentication data is still client-provided data. It becomes
trustworthy enough to use because the server has a way to verify it, and
because the system assumes the credential itself has not been stolen. That
verification step is the whole reason
<a href="/how-user-authentication-works/">user authentication works the way it does</a>,
whatever method sits behind it.</p>
<p>That distinction is important: <strong>the server doesn't trust the browser; it
trusts a credential that it can verify.</strong></p>
<h3 id="4-network-information">4. Network information<a class="heading-anchor" href="#4-network-information" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Then there is information that looks like it should describe the connection itself, such as the client's IP address.</p>
<p>This is where things become more complicated because the request may pass through several systems before reaching your application:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>  Visitor</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span>  Browser</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span>  CDN / proxy</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span> Load balancer</span></span>
<span class="line"><span>     ↓</span></span>
<span class="line"><span> Your server</span></span></code></pre></div>
<p>Your application may therefore learn the client's IP from a forwarded header rather than directly from the network connection.</p>
<p>That's why headers such as <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For"><code>X-Forwarded-For</code></a> need special handling. Your application needs to know which proxies it trusts before treating the
information they provide as the real client IP.</p>
<p>This is also why rate limiting based on IP can go wrong if the proxy configuration is incorrect.</p>
<p><img src="/claims-from-the-browser-versus-facts-the-server-can-verify.webp" alt="A comparison of what a server may and may not believe. On the left,
claims that arrive from the browser such as the User-Agent header, the
Referer header, form fields and hidden inputs, all marked as unverified.
On the right, facts the server can verify itself such as its own session
store, its own signature on a token, and its own database
records." style="max-width:80%" class="img-narrow"></p>
<h3 id="the-important-distinction">The important distinction<a class="heading-anchor" href="#the-important-distinction" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>All of these examples start with the same basic fact:</p>
<p><strong>The client can send data, but sending data doesn't make it true.</strong></p>
<p>The server has to decide what can be trusted directly, what needs to be
validated, and what needs some stronger form of verification. That's the real lesson behind spoofing. You don't need to memorize every
header that can be spoofed.</p>
<p>You need to recognize the pattern:</p>
<blockquote>
<p><strong>If the client controls the value, treat it as a claim until your
server has a reason to trust it.</strong></p>
</blockquote>
<h2 id="what-a-server-can-safely-trust-instead">What a server can safely trust instead<a class="heading-anchor" href="#what-a-server-can-safely-trust-instead" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>So if the client can change almost anything in a request, what can the server actually trust?</p>
<p>The answer isn't "nothing." The answer is that the server should trust things it can <strong>verify independently</strong>.</p>
<p>For example, instead of trusting the price sent by the browser, the server can take the product ID and look up the price in its own database.</p>
<p>Instead of trusting a role sent by the client:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "userId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">123</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "role"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"admin"</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
<p>the server can get the user's role from its own database or session.</p>
<p>Instead of trusting that a session ID is valid because the browser sent it, the server can look it up in its own session store.</p>
<p>Instead of trusting that a token hasn't been changed, the server can verify
its signature.</p>
<p>And instead of trusting a password sent by the client by storing it and
comparing it later, the server
<a href="/why-websites-cant-tell-you-your-password/">stores a password hash</a> and
verifies the password against that hash.</p>
<p>The pattern is the important part.</p>
<p>The server doesn't need to prove that the browser is trustworthy. It doesn't need to know whether the request came from Chrome, curl, Postman,
or a script.</p>
<p>It needs to take the claims in the request and compare them against something it can verify independently.</p>
<p>That's the kind of trust that survives the boundary.</p>
<h2 id="why-modern-frameworks-blur-client-side-and-server-side-validation">Why modern frameworks blur client-side and server-side validation<a class="heading-anchor" href="#why-modern-frameworks-blur-client-side-and-server-side-validation" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The reason this gets confusing in modern codebases is that the line between the browser and the server has become harder to see.</p>
<p>Over the last twenty years, more and more application logic has moved into the browser.</p>
<p>The <a href="/why-we-needed-jquery/">jQuery era</a> made it easy to validate forms in the browser and give users immediate feedback.</p>
<p><a href="/why-we-needed-single-page-applications/">Single-page applications</a> moved even more of the application's logic into the browser, including routing
and state.</p>
<p><a href="/why-we-needed-react/">React</a> made it natural to put logic inside components. A permission check can sit next to the button it controls and
look completely legitimate.</p>
<p>Then <a href="/why-we-needed-node-js/">Node.js</a> put the same language on both sides of the boundary. Now a validation function can look almost identical
whether it runs in the browser or on the server.</p>
<p><img src="/how-the-trust-boundary-stayed-put-as-code-moved.webp" alt="A timeline showing how much application logic moved into the browser
across the jQuery, single page application, React and Node.js eras, while
the trust boundary between browser and server stayed in exactly the same
place throughout." style="max-width:70%" class="img-narrow"></p>
<p><strong>But none of this changed the trust boundary.</strong></p>
<p>A check in a React component can improve the user experience, but it cannot protect an API. A route guard can hide the admin screen, but the admin API still has to check whether the user is actually allowed to perform the action.</p>
<p>The code moved.</p>
<p><strong>The boundary didn't.</strong></p>
<p>And this doesn't only apply to browsers.</p>
<p>If your server splits into <a href="/monolith-vs-microservices-explained/">multiple services talking over a network</a>, you've created new trust
boundaries where there used to be ordinary function calls.</p>
<p>The same question applies at every boundary:</p>
<blockquote>
<p><strong>Does this system actually control the thing it's about to believe?</strong></p>
</blockquote>
<h2 id="client-side-vs-server-side-validation-where-each-check-belongs">Client-side vs server-side validation: where each check belongs<a class="heading-anchor" href="#client-side-vs-server-side-validation-where-each-check-belongs" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>None of this means you should remove your client-side validation. Keep it. It makes forms easier to use, gives users immediate feedback, and avoids unnecessary requests. If someone enters an invalid email address, for example, the browser can point that out immediately instead of waiting for the server to respond. <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation">MDN makes the same point in its guide to form validation</a>, which is worth reading if you want the practical details.</p>
<p>The important checks, however, need to exist on the server too. Think about the things your application actually cares about:</p>
<ul>
<li>A checkout page can check that the quantity is at least 1, but the server should calculate the final price from the product in the database.</li>
<li>A frontend can hide an "Admin" button from regular users, but the server must check the user's permissions when the admin API is called. Getting this wrong is common enough that OWASP, the Open Worldwide Application Security Project, <a href="https://owasp.org/Top10/A01_2021-Broken_Access_Control/">ranks broken access control as the number one web application risk</a>.</li>
<li>A form can require a username and password, but the server still needs to validate both when the request arrives.</li>
<li>A file upload can limit the file size in the browser, but the server must enforce the limit because someone can upload the file without using your form at all.</li>
</ul>
<p>The browser's checks make the application nicer to use. The server's checks are what actually enforce the rules.</p>
<p>This isn't about distrusting everything. It's about knowing where a decision is being made and whether the thing making that decision can be controlled by the person you're trying to protect against.</p>
<p>That same idea appears throughout web security: CSRF, <a href="/why-your-api-works-in-postman-but-not-in-the-browser/">CORS</a>, API keys, rate limiting, authentication, authorization, and many other topics are all different ways of dealing with trust across boundaries.</p>
<p>Once you understand that boundary, you don't need to memorize a separate rule for every situation. You can ask the same question each time:</p>
<p><strong>Who controls this value, and how can I verify it?</strong></p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>The trust boundary sits between the browser the user controls and the server that controls the application logic.</li>
<li>Client-side validation improves the user experience, but it is not a security control. Anything enforced only by the browser can be changed or bypassed.</li>
<li>Request data is not automatically trustworthy just because it arrived over HTTP. This includes form fields, JSON data, headers, and cookies.</li>
<li>When something matters, the server should verify it independently using data or mechanisms it controls, such as its database, session store, or cryptographic signatures.</li>
<li>Modern frameworks have moved more application logic into the browser, but the trust boundary itself has not moved.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="is-client-side-validation-enough-on-its-own">Is client-side validation enough on its own?<a class="heading-anchor" href="#is-client-side-validation-enough-on-its-own" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Client-side validation gives users immediate feedback, makes forms more responsive, and can prevent unnecessary requests. But the user controls the browser, so they can change or bypass those checks. Important validation must also happen on the server.</p>
<h3 id="can-a-server-detect-if-a-request-came-from-a-real-browser">Can a server detect if a request came from a real browser?<a class="heading-anchor" href="#can-a-server-detect-if-a-request-came-from-a-real-browser" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not reliably. Headers such as User-Agent can be changed, and browser fingerprinting is still based on information provided by the client. These techniques can help with bot detection, but they should not be treated as proof that a request came from a trustworthy browser.</p>
<h3 id="does-https-mean-i-can-trust-the-request-data">Does HTTPS mean I can trust the request data?<a class="heading-anchor" href="#does-https-mean-i-can-trust-the-request-data" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. HTTPS protects the connection between the client and the server from being read or modified by someone in the middle. It does not tell you that the person or program making the request is trustworthy. A modified or malicious request can still arrive over a perfectly secure HTTPS connection.</p>
<h3 id="should-validation-happen-on-the-client-or-the-server">Should validation happen on the client or the server?<a class="heading-anchor" href="#should-validation-happen-on-the-client-or-the-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Usually, <strong>both</strong>. Put checks in the browser when they improve the user experience, such as showing that an email address is missing or a required field is empty. Then enforce the important rules again on the server.</p>
<p>For example, the browser can show an error when a username is too short, but the server should enforce the actual minimum length when the request arrives. The browser can also show a countdown before allowing a user to request another verification email, but the server must enforce the rate limit itself.</p>
<p>The two checks have different jobs: <strong>the client helps the user; the server enforces the rules.</strong></p>]]></content:encoded>
    </item>
    <item>
      <title>Cookies vs Sessions vs Tokens: How websites keep you logged in</title>
      <link>https://themissinglevel.dev/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/</guid>
      <pubDate>Sun, 09 Aug 2026 12:00:00 GMT</pubDate>
      <description>You log in once, yet the website keeps knowing who you are. Learn how sessions, cookies and tokens keep you logged in without resending your password.</description>
      <category>Security</category>
      <content:encoded><![CDATA[<p>One of the most common things you do every day is log in to websites. You enter your username and password, click <strong>Log in</strong>, and you're in.</p>
<p>Then you open a few more pages, refresh the browser, or come back an hour later. Somehow, the website still knows who you are, even though you only entered your password once.</p>
<p>That might feel completely normal, but it's actually a surprisingly interesting problem to solve.</p>
<p>In <a href="/how-user-authentication-works/">the previous article</a> of this series, we looked at the different ways websites can authenticate users. But authentication is only the first step.</p>
<p><strong>How does the website remember that it's still You on the next request?</strong></p>
<p>That's what this article is about.</p>
<h2 id="why-your-password-isnt-sent-on-every-request">Why your password isn't sent on every request<a class="heading-anchor" href="#why-your-password-isnt-sent-on-every-request" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>If you've read the previous article in this series, you already know that authentication is the process of proving your identity. But that naturally raises another question.</p>
<p>Why isn't your password sent with every request? Why is sending it only once considered the best practice?</p>
<p>At first glance, it doesn't sound like a bad idea.</p>
<p>Every time you open a page, refresh it, or click a link, your browser could simply include your username and password, and the server could verify them before sending a response.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>GET /profile</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Authentication:</span></span>
<span class="line"><span>  username: myUsername</span></span>
<span class="line"><span>  password: MySecretPassword123</span></span></code></pre></div>
<p>And it could do exactly the same thing for every request that follows.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>GET /orders</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Authentication:</span></span>
<span class="line"><span>  username: myUsername</span></span>
<span class="line"><span>  password: MySecretPassword123</span></span>
<span class="line"><span></span></span>
<span class="line"><span>GET /settings</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Authentication:</span></span>
<span class="line"><span>  username: myUsername</span></span>
<span class="line"><span>  password: MySecretPassword123</span></span></code></pre></div>
<p>It sounds simple, but it's actually a terrible idea. Why? Did you ever pause and think about it?</p>
<p>Even though HTTPS encrypts the connection between your browser and the server, sending your password over and over again gives it many more opportunities to leak. It could accidentally end up in application bugs, server logs, browser extensions, debugging tools, or anywhere else sensitive information is handled.</p>
<p>You might be wondering, <strong>"But doesn't the website <a href="/why-websites-cant-tell-you-your-password/">hash my password</a> anyway?"</strong></p>
<p>It does, but <strong>only after the password reaches the server</strong>. To verify your identity, the browser still has to send the original password every time. The more often that happens, the more opportunities there are for it to be exposed.</p>
<p>Your password really has only one job: to prove who you are. Once the website has verified your identity, there's no reason to keep sending that same secret again and again.</p>
<p>It's a bit like showing your ID when entering a private event. You show it once to prove who you are. After that, nobody asks to see it every time you walk from one room to another. They simply need another way to recognise that it's still you.</p>
<p>So if the website stops asking for your password, what <strong>does</strong> it use instead?</p>
<h2 id="so-what-replaces-your-password">So what replaces your password?<a class="heading-anchor" href="#so-what-replaces-your-password" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Once you've successfully authenticated, the website somehow has to remember who you are.</p>
<p>But why doesn't it already know?</p>
<p>After all, you authenticated just a few seconds ago.</p>
<p>The answer is that <strong>HTTP doesn't remember previous requests.</strong> Every request is treated independently, so when your browser asks for the next page, the website has no built-in way of knowing that you authenticated a few seconds ago.</p>
<p>Instead of asking for your password again, the server creates a new credential and sends it to your browser. Your browser stores it and automatically includes it with future requests, allowing the server to recognise you without asking for your password again.</p>
<p><img src="/authentication-server-credentials-token-for-browser.webp" alt="Diagram is showing how a website keeps a user logged in. The browser sends login credentials, the server returns a new credential, the browser stores it and automatically sends it with future requests, allowing the server to recognize the authenticated user."></p>
<p>Depending on how the application is built, that credential is usually one of these:</p>
<ul>
<li>A <strong>session ID</strong>, where the server remembers who you are.</li>
<li>A <strong>token</strong>, where the credential itself carries the information needed to identify you.</li>
</ul>
<p>In many applications, that credential is stored and sent inside a <strong>cookie</strong>, although cookies can be used for many other purposes besides authentication.</p>
<p>At this point you've probably noticed three terms coming up repeatedly:</p>
<ul>
<li><a href="#sessions-the-server-remembers">Sessions</a></li>
<li><a href="#cookies-how-the-browser-remembers">Cookies</a></li>
<li><a href="#tokens-the-browser-remembers">Tokens</a></li>
</ul>
<p>They're closely related, but they aren't interchangeable. Let's see what each one is and how they work together.</p>
<h2 id="sessions-the-server-remembers">Sessions: the server remembers<a class="heading-anchor" href="#sessions-the-server-remembers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>When I first heard the term <strong>session</strong>, I found it a little confusing.</p>
<p>The word made me think of a study session, something that simply has a beginning and an end. While that's not completely wrong, in web development a session usually refers to something much more specific.</p>
<p>A <strong>session</strong> is a piece of information the server creates after you've successfully authenticated. It allows the server to remember who you are without asking for your password on every request.</p>
<p>Because this information lives on <a href="/what-is-a-server-really/">the server</a>, you'll often hear it called a <strong>server session</strong>. In practice, though, developers usually shorten it to simply <strong>session</strong>, and both terms normally mean the same thing.</p>
<p>You can think of it as a small record that stores information about your authenticated state.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Session ID: abc123xyz</span></span>
<span class="line"><span></span></span>
<span class="line"><span>User: John</span></span>
<span class="line"><span>Authenticated: Yes</span></span>
<span class="line"><span>Created: 10:32</span></span>
<span class="line"><span>Expires: 18:32</span></span></code></pre></div>
<p>Notice that this record contains much more than just your identity. The server can store almost anything it needs for the current session, such as when you authenticated, when the session expires, your permissions, or other temporary information.</p>
<p>Depending on the application, these sessions might be stored in memory, Redis, or <a href="/what-is-a-database/">a database</a>. Wherever they're stored, the idea is exactly the same: the server keeps the information and uses the session ID to find it again.</p>
<p>The important thing to understand is that <strong>this session never leaves the server</strong>.</p>
<blockquote>
<p>Session never leaves the server</p>
</blockquote>
<p>Instead, the server creates a unique <strong>session ID</strong> that identifies it.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Session ID → abc123xyz</span></span></code></pre></div>
<p>Think of the session ID as the session's unique identifier. The browser doesn't need the whole session; it only needs a way to tell the server which session belongs to it. Keeping the real data on the server side is deliberate rather than incidental, because <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a> with anything it would be willing to believe later.</p>
<p>Whenever your browser makes another request, it sends the session ID back to the server. The server looks up the matching session and, if it finds one, immediately knows who you are without asking for your password again.</p>
<p>At this point you might be wondering:</p>
<p><strong>"If the session ID is enough to identify me, what happens if someone steals it?"</strong></p>
<p>That's a great question, and the short answer is that <strong>yes</strong>, anyone who obtains a valid session ID may be able to impersonate you until that session expires or is invalidated. That's why protecting session IDs is just as important as protecting passwords.</p>
<p>Fortunately, modern browsers and websites include several layers of protection to make stealing session IDs much harder. We'll come back to those shortly.</p>
<p>But before we can understand how they're protected, we first need to answer another question:</p>
<p><em>How does the browser remember and automatically send the session ID with every request?</em></p>
<h2 id="cookies-how-the-browser-remembers">Cookies: how the browser remembers<a class="heading-anchor" href="#cookies-how-the-browser-remembers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>We've now seen that the server creates a session and sends the browser a <strong>session ID</strong>.</p>
<p>The next logical question would be: <em>Where does the browser actually keep that session ID?</em></p>
<p>Browsers can store information in several places, for example:</p>
<ul>
<li><strong>Cookies</strong>, small pieces of data that are automatically sent with requests to the same website.</li>
<li><strong>Local storage</strong>, data that's stored in the browser and remains there until it's removed.</li>
<li><a href="/session-vs-session-cookie-vs-sessionstorage/"><strong>Session storage</strong></a>, similar to local storage, but it's cleared when you close the browser tab.</li>
</ul>
<p>When websites use sessions, the session ID is most commonly stored in a <strong>cookie</strong>.</p>
<p>When I first learned about cookies, I thought they existed only for authentication.</p>
<p>Ironically, most of us had already heard about cookies for years because of those cookie banners on almost every website, without actually knowing what cookies were. A <strong>cookie</strong> is simply a small piece of data that a website asks the browser to store. Once stored, the browser automatically includes that cookie in future requests to the same website, using the <a href="/seven-http-request-headers-every-developer-should-understand/#cookie-header"><code>Cookie</code> request header</a>.</p>
<p>For example, after you authenticate, the server might respond with something like this:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Set-Cookie:</span></span>
<span class="line"><span>sessionId=abc123xyz</span></span></code></pre></div>
<p>The browser <a href="/why-your-cookie-is-not-being-set/">stores that cookie automatically</a>, assuming nothing about the cookie breaks its rules.</p>
<p><img src="/how-session-cookies-work.webp" alt="Diagram showing how a website keeps a user logged in using sessions and cookies. The server creates a session, sends a session ID in a Set-Cookie header, the browser stores it, and automatically includes it with future requests."></p>
<p>From that point on, every request to the same website includes it automatically too.</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>GET /profile</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Cookie:</span></span>
<span class="line"><span>sessionId=abc123xyz</span></span></code></pre></div>
<p>The server reads the session ID from the cookie, looks up the matching session, and immediately knows who you are.</p>
<p>Notice that, as we discussed earlier, the cookie doesn't contain the session itself. It only contains the <strong>session ID</strong>. The actual session always remains on the server.</p>
<p>We've now seen the classic solution.</p>
<p>The server remembers who you are by storing a session, while the browser remembers which session belongs to you by storing its session ID in a cookie.</p>
<p>It's a simple approach, but it also means the server has to keep a session for every authenticated user.</p>
<p>What if it didn't have to?</p>
<p>That's where tokens come in.</p>
<h2 id="tokens-the-browser-carries-the-information">Tokens: the browser carries the information<a class="heading-anchor" href="#tokens-the-browser-carries-the-information" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Sessions aren't the only way to keep users logged in.</p>
<p>Instead of asking the server to remember every authenticated user, some applications take a different approach. They give the browser a <strong>token</strong> that represents the authenticated user.</p>
<p>A token is simply a piece of data that the browser sends with future requests. Unlike a session ID, which is just an identifier, a token usually carries information about the authenticated user.</p>
<p>Conceptually, you can think of a token as containing information like this:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>User ID: 123</span></span>
<span class="line"><span>Role: Admin</span></span>
<span class="line"><span>Expires: 18:32</span></span></code></pre></div>
<p>Of course, that's <strong>not</strong> how it's actually sent over the network. Real tokens are encoded into a compact string that the browser sends with each request. One of the most common formats is the <a href="https://www.jwt.io/">JSON Web Token (JWT)</a>, which looks something like this:</p>
<div class="code-block"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...</span></span></code></pre></div>
<p>Don't worry about understanding that string yet. In the next article, we'll break it apart, piece by piece, and see exactly what's inside it.</p>
<p>For now, the important thing is understanding how tokens differ from sessions.</p>
<p>The following diagram shows the main difference between the two approaches.</p>
<p><img src="/sessions-vs-tokens-authentication-flow.webp" alt="Diagram comparing session-based and token-based authentication. With sessions, the server looks up user data using a session ID. With tokens, the server verifies the token and reads the user information it contains."></p>
<p>The main difference is this.</p>
<p>With <strong>sessions</strong>, the browser sends a session ID and the server uses it to find the matching session.</p>
<p>With <strong>tokens</strong>, the browser sends the token itself. Before trusting it, the server verifies that the token is genuine and hasn't been modified. If the verification succeeds, the server reads the information inside the token and identifies the user.</p>
<p>One of the biggest advantages of tokens is that the server doesn't have to keep a session for every authenticated user. This makes them especially popular for APIs, mobile applications, and distributed systems where <strong>many different</strong> servers may handle the same user's requests.</p>
<p>Just like a session ID, a token can also be stored in a cookie. It can also be stored somewhere else, such as local storage, depending on how the application is designed.</p>
<h2 id="sessions-vs-tokens-when-to-use-each">Sessions vs tokens: when to use each?<a class="heading-anchor" href="#sessions-vs-tokens-when-to-use-each" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Both approaches solve exactly the same problem: remembering who the authenticated user is between requests without asking for their password again.</p>
<p>The biggest difference is <strong>where the authentication information lives</strong>.</p>
<table>
<thead>
<tr>
<th scope="col">Question</th>
<th scope="col">Sessions</th>
<th scope="col">Tokens</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Where is the information stored?</th>
<td>On the server</td>
<td>Inside the token</td>
</tr>
<tr>
<th scope="row">What does the browser send?</th>
<td>A session ID</td>
<td>A token</td>
</tr>
<tr>
<th scope="row">What does the server do?</th>
<td>Looks up the session (memory, Redis, database)</td>
<td>Verifies the token and reads the information it contains</td>
</tr>
<tr>
<th scope="row">Does the server keep user state?</th>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<th scope="row">Common use cases</th>
<td>Traditional web applications</td>
<td>APIs, SPAs, mobile apps, distributed systems</td>
</tr>
</tbody>
</table>
<p>Neither approach is universally better. The right choice depends on the type of application you're building.</p>
<p>Sessions are often the simplest and most common choice for websites where the server generates the pages. Since the server is already handling every request, keeping a session for each authenticated user is usually straightforward.</p>
<p>Tokens are especially popular for APIs, mobile applications, and distributed systems. Because the authentication information travels with the token, any server that receives the request can verify it without having to look up a shared session. This makes it much easier to scale applications across multiple servers.</p>
<p>One final thing that's worth remembering is that these three terms describe completely different concepts.</p>
<p>Although we've used them in the context of authentication throughout this article, they're much broader than that and can be used in many different situations.
s</p>
<ul>
<li>A <strong>session</strong> is a way for the server to store information between requests.</li>
<li>A <strong>cookie</strong> is a browser feature for storing information and automatically sending it with future requests.</li>
<li>A <strong>token</strong> is a piece of data that represents or carries information.</li>
</ul>
<p>In authentication, these concepts simply become tools that help websites recognise who you are and keep you logged in.</p>
<p>If you remember those three definitions, you'll avoid one of the most common sources of confusion when learning how authentication works.</p>
<h2 id="explore-the-series">Explore the series<a class="heading-anchor" href="#explore-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>We've now seen how websites remember who you are after you've authenticated.</p>
<p>In the next article, we'll take a closer look at <strong>JSON Web Tokens (JWTs)</strong>. We'll open up a real token, see what's actually inside it, learn how it's signed, and understand why servers can trust it without storing a session.</p>
<ul>
<li><a href="/how-user-authentication-works/">How user authentication works: the main methods explained</a></li>
<li><strong>Cookies vs Sessions vs Tokens: How websites keep you logged in</strong> <em>(You're reading this)</em></li>
<li>JWT authentication explained: What's actually inside a token? <em>(Coming soon)</em></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>How user authentication works: the main methods explained</title>
      <link>https://themissinglevel.dev/how-user-authentication-works/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/how-user-authentication-works/</guid>
      <pubDate>Wed, 05 Aug 2026 21:00:00 GMT</pubDate>
      <description>User authentication is how a website proves you are who you claim to be. Learn the methods, from passwords to passkeys and MFA, and the tradeoffs of each.</description>
      <category>Security</category>
      <content:encoded><![CDATA[<p>Every account you own depends on one simple question that a website has to answer before it does anything else: <strong>Is the person making this request really who they say they are?</strong></p>
<p>The process of answering that question is called <strong>authentication</strong>. We go through it every day without giving it much thought. You type a password, approve a notification on your phone, or scan your fingerprint, and a few moments later you're logged in.</p>
<p>But what is the website actually checking?</p>
<p>In this article, we'll look at what authentication really means, how it differs from two other security terms that people often mix it up with, and the different ways modern applications can verify your identity. The goal isn't to learn how to build a login form, but to understand why so many authentication methods exist and what each one is trading away. Once you understand those trade-offs, choosing between them becomes much easier.</p>
<p>One thing this article doesn't cover is what happens <strong>after</strong> you successfully log in. Proving who you are is only the first step. <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">Keeping you logged in across future requests</a> is a different problem with its own design.</p>
<p>Here, we're only focusing on the moment where you prove your identity.</p>
<h2 id="what-authentication-actually-means">What authentication actually means<a class="heading-anchor" href="#what-authentication-actually-means" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Authentication is the process of proving your identity. You claim to be a particular user, and the website asks for evidence before it accepts that claim. The reason evidence is needed at all is that the claim arrives from outside, and <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a> to be honest about who is sitting behind it.</p>
<p><img src="/authentication-authorization-flow.webp" alt="The authentication flow between a user and a website: the user claims an identity, the website asks them to prove it, the user provides evidence such as a password or passkey, the website verifies the evidence, and access is granted."></p>
<blockquote>
<p>Authenticate = Prove you are who you say you are.</p>
</blockquote>
<p>That evidence comes in the form of an <strong>authentication factor</strong>. A factor is simply a type of evidence you can provide, and almost every authentication method you've ever used fits into one of these three categories:</p>
<ul>
<li><strong>Something you know</strong>, like a password or PIN.</li>
<li><strong>Something you have</strong>, like your phone or a hardware security key.</li>
<li><strong>Something you are</strong>, like your fingerprint or face.</li>
</ul>
<p>Each factor has its own weaknesses. Passwords can be guessed or stolen, phones and security keys can be lost, and biometrics can't be changed if they're ever compromised. That's why modern authentication often combines multiple factors instead of relying on just one.</p>
<h2 id="authentication-vs-identification-vs-authorization-whats-the-difference">Authentication vs identification vs authorization: what's the difference?<a class="heading-anchor" href="#authentication-vs-identification-vs-authorization-whats-the-difference" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Now that we know what authentication is, let's clear up two other terms that are often confused with it: <strong>identification</strong> and <strong>authorization</strong>.</p>
<p><strong>Identification</strong> is simply telling the website who you are, for example by entering your email address. It's only a claim, and anyone can type someone else's email address, so by itself it proves nothing.</p>
<p><strong>Authorization</strong> is a different question that only comes after authentication succeeds. Instead of asking <em>who are you?</em>, it asks <em>what are you allowed to do?</em> Can this user view this page, delete this record, or access this API?</p>
<p>Think <strong>"I See Zones"</strong> (<strong>I C Z</strong>):</p>
<ul>
<li><strong>I</strong>dentification → I identify myself as Duke.</li>
<li>Authenti<strong>C</strong>ation → The website confirms it's really me.</li>
<li>Authori<strong>Z</strong>ation → Now that it knows who I am, it decides which zones I can access.</li>
</ul>
<p>Mixing these terms up can lead to real security bugs. A website might correctly verify that a user is logged in but forget to check whether they're allowed to access a particular resource, allowing them to see or modify someone else's data.</p>
<h2 id="what-are-the-main-authentication-methods">What are the main authentication methods?<a class="heading-anchor" href="#what-are-the-main-authentication-methods" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Now that we know the three authentication factors, the different login methods start to make much more sense. They aren't random technologies, they're simply different ways of proving your identity.</p>
<table>
<thead>
<tr>
<th scope="col">Method</th>
<th scope="col">Authentication factor</th>
<th align="center" scope="col">Passwordless</th>
<th align="center" scope="col">Security</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Password</th>
<td>Something you know</td>
<td align="center">❌</td>
<td align="center">⭐⭐☆☆☆</td>
</tr>
<tr>
<th scope="row">SMS OTP</th>
<td>Something you have</td>
<td align="center">✅</td>
<td align="center">⭐⭐☆☆☆</td>
</tr>
<tr>
<th scope="row">Email OTP</th>
<td>Something you have</td>
<td align="center">✅</td>
<td align="center">⭐⭐☆☆☆</td>
</tr>
<tr>
<th scope="row">Authenticator app (TOTP)</th>
<td>Something you have</td>
<td align="center">✅</td>
<td align="center">⭐⭐⭐⭐☆</td>
</tr>
<tr>
<th scope="row">Magic link</th>
<td>Something you have</td>
<td align="center">✅</td>
<td align="center">⭐⭐⭐☆☆</td>
</tr>
<tr>
<th scope="row">Push notification</th>
<td>Something you have</td>
<td align="center">✅</td>
<td align="center">⭐⭐⭐⭐☆</td>
</tr>
<tr>
<th scope="row">Passkey (WebAuthn)</th>
<td>Something you have + Something you are*</td>
<td align="center">✅</td>
<td align="center">⭐⭐⭐⭐⭐</td>
</tr>
<tr>
<th scope="row">Social login</th>
<td>Delegates authentication to another provider</td>
<td align="center">✅</td>
<td align="center">⭐⭐⭐⭐☆</td>
</tr>
</tbody>
</table>
<p><em>*Most passkeys are unlocked with a fingerprint or face, combining two authentication factors in a single step.</em></p>
<p>We'll start with the simplest methods and gradually move to the more advanced ones:</p>
<ul>
<li><a href="#passwords">Passwords</a></li>
<li><a href="#one-time-password-otp">One-time password (OTP)</a>
<ul>
<li><a href="#sms-codes">SMS codes</a></li>
<li><a href="#email-codes">Email codes</a></li>
<li><a href="#authenticator-apps-totp">Authenticator apps (TOTP)</a></li>
</ul>
</li>
<li><a href="#magic-links">Magic links</a></li>
<li><a href="#push-notifications">Push notifications</a></li>
<li><a href="#passkeys">Passkeys</a></li>
<li><a href="#social-login">Social login</a></li>
</ul>
<h3 id="passwords">Passwords<a class="heading-anchor" href="#passwords" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Passwords are the most common form of authentication and the classic example of <strong>something you know</strong>.</p>
<p>When you enter your password, the website verifies it before allowing you to sign in. A well-designed website never stores your actual password. Instead, it stores a <strong>password hash</strong>, a one-way mathematical transformation created using algorithms such as <strong>bcrypt</strong> or <strong>Argon2</strong>.</p>
<p>When you log in, your password is hashed again and the two hashes are compared. If they match, the website knows you've entered the correct password without ever needing to store or recover the original one.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Your password</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span>Hashing algorithm (bcrypt / Argon2)</span></span>
<span class="line"><span>      ↓</span></span>
<span class="line"><span>Password hash stored in the database</span></span></code></pre></div>
<p>If you'd like to understand why this works, and why websites can't recover your original password, I explain it in <a href="/why-websites-cant-tell-you-your-password/">why websites can't tell you your password</a>.</p>
<p>Their biggest advantage is simplicity. They don't require any special hardware, everyone knows how to use them, and they're supported almost everywhere.</p>
<p>Their biggest weakness is us. People reuse passwords across websites, choose weak ones, or get tricked into entering them on fake login pages.</p>
<h3 id="one-time-password-otp">One-time password (OTP)<a class="heading-anchor" href="#one-time-password-otp" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A <strong>one-time password (OTP)</strong> is a short code that can only be used once. Even if someone steals the code after you've used it, it becomes useless because it has already expired or has already been used.</p>
<p>OTPs can be used in two ways:</p>
<ul>
<li><strong>Instead of a password</strong>, as the primary way to authenticate.</li>
<li><strong>Together with a password</strong>, as <strong>two-factor authentication (2FA)</strong> or <strong>multi-factor authentication (MFA)</strong>.</li>
</ul>
<p>MFA is simply the more general term for using two or more authentication factors. Two-factor authentication (2FA) is the most common type of MFA.</p>
<p>In the case of 2FA/MFA, after entering your password, you're asked for a second piece of information, such as a code from your phone. This makes your account much more secure because an attacker now needs two different authentication factors instead of just one.</p>
<p>The three most common ways to receive or generate an OTP are:</p>
<ul>
<li>SMS codes</li>
<li>Email codes</li>
<li>Authenticator apps (TOTP)</li>
</ul>
<h4 id="sms-codes">SMS codes<a class="heading-anchor" href="#sms-codes" data-heading-anchor="" aria-label="Copy link to this section">#</a></h4>
<p>SMS codes are a common form of <strong>something you have</strong>.</p>
<p>When you set it up, you register your phone number with the website. Whenever it needs to verify your identity, it generates a one-time code and sends it as a text message to that number.</p>
<p>For example:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Your ExampleSite verification code is 482913.</span></span>
<span class="line"><span>This code expires in 5 minutes.</span></span></code></pre></div>
<p>The security of SMS authentication depends on your phone number. If someone manages to take over that number, they can receive your verification codes instead. One way this can happen is through <strong>SIM swapping</strong>, where an attacker transfers your phone number to another SIM card.</p>
<p>We'll see why authenticator apps and passkeys avoid this problem in the next sections.</p>
<h4 id="email-codes">Email codes<a class="heading-anchor" href="#email-codes" data-heading-anchor="" aria-label="Copy link to this section">#</a></h4>
<p>Email codes also use <strong>something you have</strong>, but instead of sending the code by SMS, the website sends it to your email inbox.</p>
<p>This is generally more convenient, but your account is now only as secure as your email account. If someone gains access to your email, they can receive your login codes too.</p>
<p>When you register your email address, the website can send you a one-time code whenever it needs to verify your identity.</p>
<p>Same as sms code but in your email:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Your ExampleSite verification code is 482913.</span></span>
<span class="line"><span>This code expires in 5 minutes.</span></span></code></pre></div>
<p>Entering the correct code proves you have access to the registered email account.</p>
<h4 id="authenticator-apps-totp">Authenticator apps (TOTP)<a class="heading-anchor" href="#authenticator-apps-totp" data-heading-anchor="" aria-label="Copy link to this section">#</a></h4>
<p>Authenticator apps such as <strong>Google Authenticator</strong> or <strong>Microsoft Authenticator</strong> are installed on your phone and are used as a second authentication factor.</p>
<p><strong>How it works</strong></p>
<ol>
<li>Install an authenticator app on your phone.</li>
<li>On the website, enable two-factor authentication and scan the QR code with the app.</li>
<li>The website and the app now share a unique secret that's linked to your account.</li>
<li>Every 30 seconds, both use that shared secret together with the current time to independently calculate a new six-digit code. Since they both start with the same information, they always arrive at the same result without communicating with each other.</li>
</ol>
<p>These codes are called <strong>Time-based One-Time Passwords (TOTP)</strong> because a new code is generated every 30 seconds using the current time.</p>
<p>The shared secret never leaves your device after setup, and no code is sent over the network. This makes authenticator apps much more secure than SMS or email codes.</p>
<p>If you lose the device containing your authenticator app, you may also lose access to your codes. That's why most websites provide backup codes or other recovery methods that you should save when enabling two-factor authentication.</p>
<h3 id="magic-links">Magic links<a class="heading-anchor" href="#magic-links" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A magic link removes the need for a password. Instead, you enter your email address and receive a one-time link. Clicking that link proves you have access to your email account and authenticates you automatically.</p>
<p>When you request a magic link, the website generates a unique link that's tied to your account and sends it to your email. Opening that email and clicking the link is enough to prove your identity.</p>
<p>Like email OTPs, magic links rely on <strong>something you have</strong>, access to your email account. The only difference is that instead of typing a code, you simply click a secure link.</p>
<p>The biggest advantage is convenience. There's no password to remember and no code to copy.</p>
<p>The trade-off is that your account is only as secure as your email account. If someone gains access to your inbox, they can also use the magic links sent to you.</p>
<h3 id="push-notifications">Push notifications<a class="heading-anchor" href="#push-notifications" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Push-based authentication is commonly used as part of <strong>two-factor authentication (2FA)</strong>, but it's also used to approve sensitive actions such as payments, password changes, or signing in from a new device.</p>
<p>Whenever the website needs you to confirm your identity or approve an action, it sends a notification to your registered device. The app displays a message such as <strong>"Are you trying to sign in?"</strong> or <strong>"Approve this payment?"</strong>, and you simply tap <strong>Approve</strong> or <strong>Deny</strong>.</p>
<p>Like authenticator apps, this is an example of <strong>something you have</strong>. Since you never have to type a code, it's both convenient and much harder for attackers to trick you into entering your credentials on a fake website.</p>
<p>One weakness is <strong>prompt fatigue</strong>. If attackers repeatedly trigger authentication or approval requests, some users eventually tap <strong>Approve</strong> just to stop the notifications, even though they weren't the ones initiating the action.</p>
<h3 id="passkeys">Passkeys<a class="heading-anchor" href="#passkeys" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Passkeys are one of the biggest improvements in authentication in recent years. They were designed to replace passwords with something that's both easier to use and much more secure.</p>
<p>Instead of creating a password that you have to remember, your device creates two cryptographic keys that belong together. One key is stored by the website, while the other stays securely on your device and never leaves it.</p>
<p><img src="/authentication-using-passkeys.webp" alt="Passkey authentication flow: the private key stays on your device while the website stores the public key. Signing in sends a challenge, you approve it with your fingerprint or PIN, and your device uses its key to prove your identity so the website signs you in."></p>
<p>Whenever you authenticate, your device uses its key to show the website that it's really you. Because the secret key never leaves your device, there's no password to steal, reuse, or accidentally enter on a fake website.</p>
<p>This is what makes passkeys phishing-resistant. Since there's no shared secret to type, there's nothing for a fake login page to capture, even if you're tricked into visiting one.</p>
<p>Most passkeys are unlocked using your fingerprint or face. That means they combine <strong>something you have</strong> (your device) with <strong>something you are</strong> (your biometric), giving you a fast login without sacrificing security.</p>
<h3 id="social-login">Social login<a class="heading-anchor" href="#social-login" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Buttons like <strong>Continue with Google</strong>, <strong>Sign in with GitHub</strong>, or <strong>Continue with Apple</strong> are examples of social login.</p>
<p>Instead of creating another password, you simply use an account you already have with <strong>Google, GitHub, Apple, or another provider</strong> to prove your identity.</p>
<p>Behind the scenes, this is powered by <strong>OAuth</strong> and <strong>OpenID Connect</strong>.</p>
<p>The biggest advantage is reducing the number of passwords you need to create and maintain.</p>
<p>The trade-off is that the website no longer verifies your identity itself, it trusts another provider to do that on its behalf. If that provider becomes unavailable or your account is compromised, it can affect your access to every website that relies on it.</p>
<h2 id="different-methods-same-goal">Different methods, same goal<a class="heading-anchor" href="#different-methods-same-goal" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Every authentication method we've looked at answers the same question:</p>
<blockquote>
<p><strong>"How can the website be confident that it's really you?"</strong></p>
</blockquote>
<p>Passwords, one-time codes, magic links, push notifications, passkeys, and social login all solve that problem in different ways. Some prioritize convenience, others prioritize security, and many try to balance both.</p>
<p>Once your identity has been verified, though, the authentication process is over. The next challenge is remembering that you've already proved who you are without asking you to do it again on every request.</p>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="why-do-some-websites-still-use-passwords">Why do some websites still use passwords?<a class="heading-anchor" href="#why-do-some-websites-still-use-passwords" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Passwords are simple, familiar, and work on every device. Although passkeys are generally more secure, many websites continue to support passwords for compatibility and because migrating existing users takes time.</p>
<h3 id="can-a-website-use-more-than-one-authentication-method">Can a website use more than one authentication method?<a class="heading-anchor" href="#can-a-website-use-more-than-one-authentication-method" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. Many websites let you choose between multiple methods, such as a password, a passkey, or social login. Some also combine methods, for example by asking for a password followed by a one-time code as part of two-factor authentication.</p>
<h3 id="which-authentication-method-is-the-most-secure">Which authentication method is the most secure?<a class="heading-anchor" href="#which-authentication-method-is-the-most-secure" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>There isn't a single answer because it depends on the situation. In general, passkeys are currently one of the strongest options because they resist phishing and don't rely on shared secrets like passwords.</p>
<h3 id="what-happens-if-i-lose-my-phone">What happens if I lose my phone?<a class="heading-anchor" href="#what-happens-if-i-lose-my-phone" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Most services provide recovery methods, such as backup codes, recovery keys, or an alternative authentication method. It's a good idea to set these up before you need them.</p>
<h3 id="can-i-use-biometrics-without-passkeys">Can I use biometrics without passkeys?<a class="heading-anchor" href="#can-i-use-biometrics-without-passkeys" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. Biometrics can unlock many different secrets stored on your device, not just passkeys.</p>
<h2 id="continue-the-series">Continue the series<a class="heading-anchor" href="#continue-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>You've seen how websites verify your identity. The next step is understanding how they remember it.</p>
<p>The articles below build on one another, following what happens after authentication succeeds.</p>
<ul>
<li><strong>How user authentication works: the main methods</strong> <em>(You're reading this)</em></li>
<li><a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">Cookies vs Sessions vs Tokens: How websites keep you logged in</a></li>
<li>JWT authentication explained <em>(Coming soon)</em></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Monolith vs microservices, explained with examples</title>
      <link>https://themissinglevel.dev/monolith-vs-microservices-explained/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/monolith-vs-microservices-explained/</guid>
      <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
      <description>Monolith or microservices? The real difference is simpler than the debate suggests. See what each one means, with examples, and when to pick which.</description>
      <category>Software Engineering</category>
      <content:encoded><![CDATA[<p>Few topics in software architecture cause as much confusion as monoliths and microservices. The words show up in job listings, in conference talks, and in that one senior engineer's plan to "break the monolith apart" this quarter. Somewhere along the way the two words stopped describing a technical choice and started sounding like a moral one, where the monolith is the mess you inherited and microservices are the clean future you're supposed to want.</p>
<p>Most of that framing is wrong, and it makes the decision harder than it needs to be.</p>
<p>The honest answer is that <strong>neither one is better</strong>. They are simply two different ways of building and deploying an application. Each one solves certain problems while introducing others.</p>
<p>Once you understand those trade-offs, the discussion becomes much simpler. You can look at the problems you're trying to solve and choose the approach that fits best.</p>
<h2 id="the-one-question-that-actually-separates-them">The one question that actually separates them<a class="heading-anchor" href="#the-one-question-that-actually-separates-them" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Forget the debate for a moment. The real difference comes down to one simple question:</p>
<blockquote>
<p>"When you deploy your application, are you deploying one unit or many?"</p>
</blockquote>
<p>A <strong>monolith</strong> is one deployable unit. All of your features live in one codebase, they are built together, and they ship together as a single running program. <strong>Microservices</strong> split that same application into many small programs, each one built and deployed on its own, each one responsible for a slice of the whole.</p>
<p>That is the entire distinction. Not code quality, not team size, not how modern the stack is. It is a question of how many independent pieces you deploy. Everything else people argue about follows from that one fact, so it is worth holding onto as we go.</p>
<h2 id="what-a-monolith-actually-looks-like">What a monolith actually looks like<a class="heading-anchor" href="#what-a-monolith-actually-looks-like" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Picture a typical online store. It has user accounts, a product catalog, a shopping cart, an order system, and payments. In a monolith, all five of those live in the same codebase and run as one process.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>        Online store (one deploy)</span></span>
<span class="line"><span>        ┌───────────────────────┐</span></span>
<span class="line"><span>        │  Users                │</span></span>
<span class="line"><span>        │  Products             │</span></span>
<span class="line"><span>        │  Cart                 │</span></span>
<span class="line"><span>        │  Orders               │</span></span>
<span class="line"><span>        │  Payments             │</span></span>
<span class="line"><span>        └───────────┬───────────┘</span></span>
<span class="line"><span>                    ↓</span></span>
<span class="line"><span>              One database</span></span></code></pre></div>
<p>When the cart calls the product catalog to check a price, that is a normal function call inside the same program. The data all lives in <a href="/what-is-a-database/">one database</a>, so an order can reference a user and a product with a simple join. To deploy, you build the whole thing once and start it on <a href="/what-is-a-server-really/">a server</a>. If you need more capacity, you run several copies of that same program behind a load balancer.</p>
<p>This is how most applications start, and for good reason. Everything is in one place, so the code is easy to run on your laptop, easy to test end to end, and easy to reason about because a single request travels through one program you can step through. The monolith gets a bad reputation, but a huge number of successful products run happily as one well organized codebase for years.</p>
<p>As applications and teams grew, deploying one large application for every change became increasingly painful. Developers started looking for ways to split large systems into smaller, independently deployable pieces.</p>
<h2 id="what-microservices-actually-look-like">What microservices actually look like<a class="heading-anchor" href="#what-microservices-actually-look-like" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Now take that same store and split it into smaller, independent pieces. Users become one service. Products become another. Cart, orders, and payments each become their own service, deployed independently and often backed by their own database.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>                      network calls</span></span>
<span class="line"><span>    ┌───────────┬───────────┬──────────┬────────────┐</span></span>
<span class="line"><span>    │           │           │          │            │</span></span>
<span class="line"><span>┌───┴───┐  ┌────┴─────┐  ┌──┴───┐  ┌───┴────┐  ┌────┴─────┐</span></span>
<span class="line"><span>│ Users │  │ Products │  │ Cart │  │ Orders │  │ Payments │</span></span>
<span class="line"><span>└───┬───┘  └────┬─────┘  └──┬───┘  └───┬────┘  └────┬─────┘</span></span>
<span class="line"><span>    ↓           ↓           ↓          ↓            ↓</span></span>
<span class="line"><span>  own db      own db      own db     own db       own db</span></span></code></pre></div>
<p>The features are the same, but the wiring changed completely. When the cart needs a product price, it no longer makes a function call. It makes <strong>a network request</strong> to the products service and waits for a response <a href="/seven-http-request-headers-every-developer-should-understand/">over HTTP</a> or a message queue. When orders needs to know who a user is, it asks the users service instead of joining a table.</p>
<p>The biggest advantage is independence. The payments team can deploy a fix at 2pm without touching the catalog. If checkout traffic spikes, you can run twenty copies of the orders service while leaving everything else at one copy each.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>                  Before</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  ┌──────────┐   ┌──────────┐   ┌──────────┐</span></span>
<span class="line"><span>  │  Orders  │   │ Products │   │ Payments │</span></span>
<span class="line"><span>  └──────────┘   └──────────┘   └──────────┘</span></span>
<span class="line"><span>       ×1             ×1             ×1</span></span>
<span class="line"><span></span></span>
<span class="line"><span></span></span>
<span class="line"><span>           Checkout traffic spikes</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  ┌──────────┐   ┌──────────┐   ┌──────────┐</span></span>
<span class="line"><span>  │  Orders  │   │ Products │   │ Payments │</span></span>
<span class="line"><span>  ├──────────┤   └──────────┘   └──────────┘</span></span>
<span class="line"><span>  │  Orders  │        ×1             ×1</span></span>
<span class="line"><span>  ├──────────┤</span></span>
<span class="line"><span>  │  Orders  │   only the service under load</span></span>
<span class="line"><span>  ├──────────┤   scales, the rest stay at ×1</span></span>
<span class="line"><span>  │  Orders  │</span></span>
<span class="line"><span>  ├──────────┤</span></span>
<span class="line"><span>  │  Orders  │</span></span>
<span class="line"><span>  └──────────┘</span></span>
<span class="line"><span>       ×5</span></span></code></pre></div>
<p>Interestingly, software often ends up reflecting the way teams are organized, an observation known as <strong>Conway's Law</strong>.</p>
<blockquote>
<p><strong>Conway's Law:</strong> Organizations design systems that mirror the way they communicate.</p>
<p>Melvin Conway, 1967</p>
</blockquote>
<p>A company with separate payments, catalog, and orders teams will often build separate services for those areas too.</p>
<p>Teams can even choose different languages or databases per service, because the only contract between them is the network interface they agree on. That contract is also a boundary, which means each service has to decide how much it believes what the others tell it, in the same way <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a> just because the request looks well formed.</p>
<p>Before we go deeper, it helps to see the two approaches side by side. This is the same story the rest of the article tells, condensed into a single view.</p>
<table>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Monolith</th>
<th scope="col">Microservices</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Deployable units</th>
<td>One application</td>
<td>Many small services</td>
</tr>
<tr>
<th scope="row">How code talks</th>
<td>Direct function calls</td>
<td>Requests over the network</td>
</tr>
<tr>
<th scope="row">Data</th>
<td>Usually one shared database</td>
<td>Often one database per service</td>
</tr>
<tr>
<th scope="row">Scaling</th>
<td>Run more copies of the whole app</td>
<td>Scale each service on its own</td>
</tr>
<tr>
<th scope="row">Main cost</th>
<td>Everything ships together</td>
<td>Network failures and data spread across services</td>
</tr>
<tr>
<th scope="row">Best fit</th>
<td>A single team shipping one product</td>
<td>Many teams with very different needs</td>
</tr>
</tbody>
</table>
<h2 id="the-part-that-confused-me-most">The part that confused me most<a class="heading-anchor" href="#the-part-that-confused-me-most" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>For a long time, I thought <strong>"monolith"</strong> was just a polite word for messy code and <strong>"microservices"</strong> meant clean, well-designed software. I also thought that if you split an application into separate projects or folders, such as <code>client</code>, <code>server</code>, <code>db</code>, and <code>shared</code>, you had somehow built microservices.</p>
<p>Both ideas are wrong.</p>
<p>A monolith can be beautifully organized, with clear internal modules that barely know about each other. People often call this a <strong>modular monolith</strong>. It gives you much of the structure people associate with microservices while still deploying as a single application.</p>
<p>The opposite is also true. Microservices can be an absolute mess. If changing one service forces you to redeploy three others every time, you haven't really gained the independence microservices are supposed to provide. You've simply built a distributed monolith.</p>
<p>So what's the mental rule?</p>
<blockquote>
<p><strong>Microservices aren't about how many repos or modules you have. They're about how many applications you deploy independently.</strong></p>
</blockquote>
<p>Everything else is just how you organize your code.</p>
<p>Once that clicked for me, the whole discussion became much simpler. Code organization and deployment are two different concerns. You can have clean or messy code in either architecture.</p>
<h2 id="a-real-world-comparison">A real-world comparison<a class="heading-anchor" href="#a-real-world-comparison" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>So far we've looked at the structure of each architecture. Now let's see how that affects everyday development.</p>
<p>Imagine your product manager asks for a new feature: support for discount codes at checkout.</p>
<p><strong>Monolith version</strong></p>
<p>In a monolith, adding the feature is straightforward. You create a discounts module, store the discount codes in the application's database, and have the orders code call it when calculating the total.</p>
<p>Because everything runs inside the same application, you can test the entire checkout flow on your machine. When you're ready, you deploy the application once, and the feature is available everywhere.</p>
<p><strong>Microservices version</strong></p>
<p>In a microservices architecture, the change is usually larger. You might create a new discounts service with its own database and deploy it independently. During checkout, the orders service now has to call the discounts service over the network to calculate the final price. That immediately raises new questions. What happens if the discounts service is slow? What if it is temporarily unavailable? Should the order continue without the discount, or should checkout fail?</p>
<p>Testing also changes. Instead of running one application, you now need several services working together. On the other hand, the discounts service can evolve independently. The team responsible for discounts can deploy improvements without coordinating a release of the rest of the application.</p>
<p>Neither approach is inherently better. The monolith makes the feature easier to build. Microservices make it easier to own, evolve, and deploy independently. That's the trade-off in its simplest form.</p>
<h2 id="the-price-of-independence">The price of independence<a class="heading-anchor" href="#the-price-of-independence" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Microservices are not a free upgrade. The independence they give you comes with extra complexity.</p>
<p>Inside a monolith, one part of the application talks to another through a simple function call. It is fast and rarely fails. In a microservices architecture, that same communication happens over the network. Network requests can be slow, fail completely, or return unexpected errors, so every service has to be prepared for those situations.</p>
<p>Data becomes more complicated too. In a monolith, everything usually lives in one database, so updating related information is straightforward. In a microservices architecture, each service often has its own database. Keeping data consistent across several independent systems is much harder.</p>
<p>Debugging also changes. Instead of following a request through one application, you now have to trace it across several services. That's why teams using microservices rely heavily on logging, monitoring, and distributed tracing to understand what is happening.</p>
<p>None of this means microservices are a bad idea. It simply means that the independence they provide comes at a cost. If that independence solves problems your team actually has, the extra complexity is worth it. If not, a monolith is often the simpler and better choice.</p>
<h2 id="monolith-or-microservices-which-should-you-choose">Monolith or microservices: which should you choose?<a class="heading-anchor" href="#monolith-or-microservices-which-should-you-choose" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>For most new applications, a <strong>modular monolith</strong> is the sensible starting point. It lets you focus on building the product instead of managing infrastructure. Development, testing, and deployment stay simple, and if you keep the code well organized, you can still split parts of the application later if the need arises.</p>
<p>Microservices start to make sense when one deployable application is no longer enough. That usually happens because of growth rather than technology. Multiple teams need to release independently, different parts of the system have very different scaling requirements, or a particular service needs to be isolated for reliability.</p>
<p>In other words, the signal isn't that your monolith feels old. The signal is that your organization or your application has outgrown a single deployment.</p>
<p>That's why many successful systems follow the same path. They start as a monolith, learn where the natural boundaries are, and only then split out the parts that genuinely benefit from becoming independent services. Starting with dozens of microservices before you have that pressure often gives you the costs long before you see the benefits.</p>
<h2 id="beyond-monoliths-and-microservices">Beyond monoliths and microservices<a class="heading-anchor" href="#beyond-monoliths-and-microservices" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Monoliths and microservices are the two architectures you'll hear about most often, but they aren't the only options. As systems grow, developers have come up with other ways of splitting applications and communicating between them.</p>
<p><strong>Serverless.</strong> Instead of running long-lived services, you deploy small functions that run only when they are needed. Platforms such as <strong>AWS Lambda</strong> and <strong>Cloudflare Workers</strong> start them on demand, scale them automatically, and stop them when they are no longer needed.</p>
<p><strong>Event-Driven Architecture.</strong> Instead of one service calling another directly over HTTP, services communicate by publishing and receiving events. A payment might publish an <strong>"Order Paid"</strong> event, and other services react to it whenever they need to.</p>
<p>Although they look different, they all explore the same basic idea: <strong>how should an application be split into independent pieces, and how should those pieces communicate?</strong></p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>The real difference between a monolith and microservices is the number of independently deployable units, not the quality of the code.</li>
<li>A monolith ships as one program with usually one database, which keeps development, testing, and reasoning simple.</li>
<li>Microservices split an application into many small programs that deploy independently and talk over the network, trading simplicity for independence.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="what-is-the-main-difference-between-a-monolith-and-microservices">What is the main difference between a monolith and microservices?<a class="heading-anchor" href="#what-is-the-main-difference-between-a-monolith-and-microservices" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A monolith is one application that you build and deploy as a single unit, while microservices split that same application into many small services that deploy independently and talk over the network. Everything else, including how you organize the code, follows from that one difference.</p>
<h3 id="is-a-monolith-bad-or-outdated">Is a monolith bad or outdated?<a class="heading-anchor" href="#is-a-monolith-bad-or-outdated" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. A monolith is simply one deployable unit, and plenty of large, successful products run as well organized monoliths.</p>
<h3 id="are-microservices-always-more-scalable">Are microservices always more scalable?<a class="heading-anchor" href="#are-microservices-always-more-scalable" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not automatically. Microservices let you scale individual pieces independently, which helps when different parts have very different load. But you can also scale a monolith by running more copies of it.</p>
<h3 id="what-is-a-modular-monolith">What is a modular monolith?<a class="heading-anchor" href="#what-is-a-modular-monolith" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A modular monolith is simply a monolith with clear internal boundaries between features. The code is organized into separate modules, but everything is still built and deployed as a single application.</p>
<p>For example, a project might be organized like this:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>my-app/</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>├── src/</span></span>
<span class="line"><span>│   ├── users/</span></span>
<span class="line"><span>│   ├── products/</span></span>
<span class="line"><span>│   ├── orders/</span></span>
<span class="line"><span>│   ├── payments/</span></span>
<span class="line"><span>│   └── shared/</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>├── database/</span></span>
<span class="line"><span>├── tests/</span></span>
<span class="line"><span>└── package.json</span></span></code></pre></div>
<p>Even though the code is split into well-defined modules, this is <strong>still a monolith</strong> because the entire application is built and deployed together. You get much of the organization people associate with microservices without the cost of distributing everything across the network.</p>
<h3 id="should-a-new-project-start-with-microservices">Should a new project start with microservices?<a class="heading-anchor" href="#should-a-new-project-start-with-microservices" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Usually not. Most new projects are better off as a monolith, ideally a modular one, because you rarely know the right service boundaries up front. It is common and healthy to start with a monolith and split out services later, once real scaling or team pressure shows you where the seams are.</p>
<h3 id="how-do-microservices-talk-to-each-other">How do microservices talk to each other?<a class="heading-anchor" href="#how-do-microservices-talk-to-each-other" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Over the network, most often through HTTP APIs or a message queue, instead of the direct function calls a monolith uses internally. That network hop is exactly what gives services their independence, and also what introduces the extra latency and failure cases you have to plan for.</p>
<h2 id="the-mental-model-to-remember">The mental model to remember<a class="heading-anchor" href="#the-mental-model-to-remember" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Monoliths and microservices are not rival teams you have to pick a side on. They are two different ways of building the same application, and the right choice depends on your scale, your team, and the problems you actually have today, not the ones you might have someday.</p>
<p>Once you stop thinking in terms of "good" and "bad" and start thinking in terms of deployments, the whole discussion becomes much simpler. You can look at your own system, decide how much independence each part really needs, weigh that against the extra complexity, and make the decision on its merits.</p>
<p>If you remember just one thing from this article, let it be this:</p>
<blockquote>
<p><strong>A monolith is one independently deployable application. Microservices are many independently deployable applications that communicate over the network.</strong></p>
</blockquote>
<p>Everything else is just a consequence of that one design choice.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why we needed Node.js</title>
      <link>https://themissinglevel.dev/why-we-needed-node-js/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-we-needed-node-js/</guid>
      <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
      <description>For years Javascript could only run inside the browser. Learn why Node.js was created and how it let Javascript finally run on the server.</description>
      <category>History of the Web</category>
      <content:encoded><![CDATA[<p>Today it feels completely normal to build an entire web application using a single language. The same Javascript that runs in the browser also runs on the server, powers the build tools, and installs its dependencies through the same package manager. A developer can move from the front end to the back end without switching languages at all.</p>
<p>For most of the web's history, that wasn't possible.</p>
<p>In the earlier articles in this series, we followed how the web became interactive. We saw how <a href="/why-we-needed-javascript/">Javascript</a> brought code into the browser, how <a href="/why-we-needed-jquery/">jQuery</a> smoothed over the differences between browsers, how <a href="/why-we-needed-single-page-applications/">Single-Page Applications</a> kept users on one continuous page, and how <a href="/why-we-needed-react/">React</a> made large, state-driven interfaces manageable.</p>
<p>Every one of those steps happened inside the browser. Node.js is the moment Javascript finally escaped it.</p>
<h2 id="javascript-was-trapped-in-the-browser">Javascript was trapped in the browser<a class="heading-anchor" href="#javascript-was-trapped-in-the-browser" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>When Javascript was created, it was designed for one place: the browser. It could respond to clicks, validate forms, and update the page, but it had no way to read a file, talk to a database, or listen for network requests. Those abilities belonged to the server, and the server spoke a different language entirely.</p>
<p>So web developers lived a double life. On the front end they wrote Javascript. On the back end they wrote PHP, Ruby, Python, or Java. A single feature often meant switching between two languages, two sets of tools, and two ways of thinking about the same problem.</p>
<p>At first glance, splitting an application along language lines can look like a clean separation. The boundary between the front end and the back end is obvious, and each side owns its own tools and conventions without stepping on the other. That tidiness was real, but it came with costs that were just as real.</p>
<p>Form validation was the clearest example. It frequently had to be written twice, once in Javascript for instant feedback in the browser and again in the server's language so the data could actually be trusted. Knowledge didn't transfer cleanly between the two halves either. A strong front-end developer wasn't automatically useful on the back end, and the reverse was just as true.</p>
<p>So why couldn't the browser's language run on the server too?</p>
<h2 id="why-couldnt-javascript-run-on-a-server">Why couldn't Javascript run on a server?<a class="heading-anchor" href="#why-couldnt-javascript-run-on-a-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Inside a browser, Javascript worked because the browser included everything it needed. It contained software that could read and execute Javascript code, and it provided built-in features for working with web pages, forms, buttons, and user interactions.</p>
<p><a href="/what-is-a-server-really/">A server</a> is a completely different environment. It doesn't display web pages or respond to button clicks. Instead, it needs to read files, accept network requests, connect to databases, and communicate with the operating system.</p>
<p>The problem was that servers didn't include software that could execute Javascript, nor did they provide the tools Javascript needed to perform server-side tasks. Without those two pieces, Javascript simply couldn't run on a server.</p>
<p>That finally changed because of the browser wars.</p>
<h2 id="two-things-had-to-happen-first">Two things had to happen first<a class="heading-anchor" href="#two-things-had-to-happen-first" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The first breakthrough came from Google.</p>
<p>In 2008, Google released Chrome with a new Javascript engine called <strong>V8</strong>. A Javascript engine is the software that reads and executes Javascript code. Every browser has one, although they use different engines. Chrome uses <strong>V8</strong>, Firefox uses <strong>SpiderMonkey</strong>, and Safari uses <strong>JavaScriptCore</strong>.</p>
<p>V8 was much faster than the engines used by older browsers, allowing websites to become faster and more interactive.</p>
<blockquote>
<p>In 2008, V8 is introduced</p>
</blockquote>
<p>Although V8 was created for Chrome, it wasn't tied to Chrome itself. It was a standalone Javascript engine that could be embedded into other applications. That meant there was no technical reason it had to stay inside a browser.</p>
<p>Javascript could now run in both the browser and on a server.</p>
<p>The second breakthrough came a year later. Ryan Dahl realized that if he took V8 out of the browser and combined it with the tools a server needs, Javascript could finally run on the server too.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>                Javascript</span></span>
<span class="line"><span>                    ↓</span></span>
<span class="line"><span>                V8 Engine</span></span>
<span class="line"><span>             ┌──────┴──────┐</span></span>
<span class="line"><span>             ↓             ↓</span></span>
<span class="line"><span>      Browser APIs    Node.js APIs</span></span>
<span class="line"><span>   (DOM, Forms...)  (Files, Network, OS)</span></span></code></pre></div>
<h2 id="nodejs-is-born">Node.js is born<a class="heading-anchor" href="#nodejs-is-born" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>In 2009, <strong>Ryan Dahl</strong> turned that idea into reality by introducing <strong>Node.js</strong>.</p>
<p>He took Google's V8 engine and combined it with APIs for reading files, handling network connections, and communicating with the operating system. For the first time, Javascript had everything it needed to run as a server language.</p>
<p>Suddenly Javascript could do the things that had always been reserved for server languages. It could read and write files, listen for incoming requests on a port, and respond to them. A language that had spent its whole life reacting to button clicks could now run a web server.</p>
<p>But giving Javascript access to the operating system wasn't the part that made Node.js revolutionary. It also introduced a different way of handling many connections at the same time, and that idea is what really set it apart.</p>
<h2 id="the-real-innovation-not-blocking">The real innovation: not blocking<a class="heading-anchor" href="#the-real-innovation-not-blocking" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Giving Javascript access to the server was only part of the story. What really made Node.js stand out was the way it handled many requests at the same time.</p>
<p>Imagine one thousand people opening your website at once. Every request might need to read a file, query a database, or call another service. The problem is that those operations take time. While the server is waiting for the result, it still has hundreds or thousands of other requests to deal with.</p>
<p>At the time, many popular web servers handled this by assigning each request its own thread or process. That way, while one request was waiting, another could continue running. It was a proven approach and worked well, but it also meant that busy servers had to manage hundreds or even thousands of threads at the same time.</p>
<p>Ryan Dahl believed there was another option.</p>
<p>Instead of creating a thread for every request, Node.js uses a single main thread to coordinate the work. When it starts a slow operation, such as reading a file or <a href="/what-is-a-database/">querying a database</a>, it doesn't stop and wait. Instead, it lets that operation continue in the background and immediately starts working on another request. When the result is ready, Node.js comes back and finishes the original task.</p>
<p>This idea, known as <strong>non-blocking I/O</strong>, wasn't invented by Node.js. Similar approaches already existed in other software. What Node.js did was bring this model to Javascript in a simple runtime that made it easy for web developers to build highly scalable servers.</p>
<p>It helps to picture a good waiter in a busy restaurant. A blocking waiter takes one table's order, walks to the kitchen, and waits there until the food is ready before serving anyone else. A non-blocking waiter hands the order to the kitchen and immediately starts serving other tables. When the food is ready, they come back and deliver it.</p>
<p>This made Node.js especially good for applications that spend much of their time waiting, such as web APIs, chat applications, and streaming services. It was less suited to CPU-intensive work, where long calculations could occupy the main thread, but for the kind of work most web applications perform, it was an excellent fit.</p>
<p>The way Node.js handled requests made it an excellent choice for many web applications. But its biggest impact wasn't technical. It changed the way developers built software.</p>
<h2 id="one-language-across-the-whole-stack">One language across the whole stack<a class="heading-anchor" href="#one-language-across-the-whole-stack" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The moment Javascript could run on the server, the double life ended.</p>
<p>Developers could now build an entire application, from front end to back end, using a single language. Validation logic could be shared instead of rewritten. Knowledge gained on one side of the application carried over to the other, making it easier for developers to move across the entire stack.</p>
<p>Server-side rendering also became much more natural. The same code that described a user interface in the browser could now run on the server to generate the initial HTML, an idea that modern frameworks such as Next.js build upon.</p>
<p>Running Javascript on the server solved one problem. Making it easy to build and share software solved another.</p>
<h2 id="npm-and-an-explosion-of-tooling">npm and an explosion of tooling<a class="heading-anchor" href="#npm-and-an-explosion-of-tooling" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Node.js arrived with a companion that turned out to be just as important: <strong>npm</strong>, its package manager.</p>
<p>npm made sharing and installing reusable pieces of code effortless. Need a library for dates, testing, or talking to a database? A single command installed it. This simplicity helped npm grow into the largest software registry in the world, and its ecosystem became one of Node.js's greatest strengths.</p>
<p>There's another consequence that's easy to miss. Even front-end developers who never build a Node.js server rely on it every day. Modern build tools such as Vite, Webpack, Babel, ESLint, and countless others run on Node.js. It quietly became the foundation of the modern Javascript ecosystem, whether or not your application ever uses Node.js in production.</p>
<h2 id="the-trade-offs-we-still-live-with">The trade-offs we still live with<a class="heading-anchor" href="#the-trade-offs-we-still-live-with" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Like every technology we've covered in this series, Node.js solved important problems but introduced new ones of its own.</p>
<p>The ease of installing packages led to deep dependency trees, where even a small project can quietly pull in hundreds of packages. The ecosystem also moves quickly, and keeping up with changing tools can be exhausting. Finally, while Node.js excels at input and output heavy workloads, applications with long-running CPU-intensive tasks require extra care because they can block the main thread.</p>
<p>Even with those trade-offs, Node.js changed web development in a way no previous technology had. It turned Javascript into a full-stack language, allowing developers to use the same language from the browser all the way to the server.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>For its first years, Javascript could only run inside the browser, which forced web developers to use a separate language on the server.</li>
<li>Google's V8 engine made it possible to run Javascript efficiently outside the browser, and Node.js added the server capabilities it needed.</li>
<li>Node.js popularized a non-blocking, event-driven model that handles many simultaneous connections efficiently.</li>
<li>With Node.js and npm, Javascript became a full-stack language and the foundation of modern web tooling.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="is-nodejs-a-programming-language">Is Node.js a programming language?<a class="heading-anchor" href="#is-nodejs-a-programming-language" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Node.js is a runtime for Javascript. It uses Google's V8 engine to execute Javascript code and provides the APIs needed to work with files, networks, and the operating system.</p>
<h3 id="is-nodejs-a-framework">Is Node.js a framework?<a class="heading-anchor" href="#is-nodejs-a-framework" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Node.js is the environment your Javascript runs in, not a framework for structuring an application. Frameworks such as Express or Next.js are built on top of Node.js to make certain kinds of applications easier to build.</p>
<h3 id="why-did-nodejs-become-so-popular">Why did Node.js become so popular?<a class="heading-anchor" href="#why-did-nodejs-become-so-popular" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>It let developers use a single language across the entire stack, it handled input and output heavy workloads efficiently, and it came with npm, an enormous ecosystem of reusable packages. Together those made it a natural default for modern web development.</p>
<h3 id="is-nodejs-good-for-heavy-calculations">Is Node.js good for heavy calculations?<a class="heading-anchor" href="#is-nodejs-good-for-heavy-calculations" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not especially. Because your code runs on a single main thread, long-running calculations can block everything else. Node.js shines at work that spends its time waiting on other systems, such as serving APIs or handling real-time connections.</p>
<h3 id="does-nodejs-replace-javascript-in-the-browser">Does Node.js replace Javascript in the browser?<a class="heading-anchor" href="#does-nodejs-replace-javascript-in-the-browser" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Browsers still run Javascript exactly as before. Node.js simply lets the same language run on the server as well, allowing developers to build entire applications with Javascript from front end to back end.</p>
<h2 id="explore-the-series">Explore the series<a class="heading-anchor" href="#explore-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>I wasn't building websites when the web was just HTML or when Javascript didn't exist. Like many developers, I learned modern frameworks first and only later became curious about how we got here.</p>
<p>This series is my way of connecting those dots. By looking back at the problems developers faced and the solutions they created, it becomes much easier to understand why today's tools exist and why the web evolved the way it did.</p>
<ul>
<li><a href="/why-we-needed-javascript/">Why we needed Javascript</a></li>
<li><a href="/why-we-needed-jquery/">Why we needed jQuery</a></li>
<li><a href="/why-we-needed-single-page-applications/">Why we needed Single-Page Applications (SPAs)</a></li>
<li><a href="/why-we-needed-react/">Why we needed React</a></li>
<li><strong>Why we needed Node.js</strong> <em>(You're reading this)</em></li>
</ul>
<h2 id="thats-the-end-for-now">That's the end... for now<a class="heading-anchor" href="#thats-the-end-for-now" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This is where our journey through the evolution of modern web development pauses, but it isn't over.</p>
<p>We've followed the web from static HTML pages to interactive applications, from handwritten DOM manipulation to React, and finally to the moment Javascript escaped the browser and became a full-stack language with Node.js.</p>
<p>There are still many stories left to tell. Technologies such as TypeScript, Babel, Webpack, Vite, Docker, and many others solved their own problems and changed the way we build software. Those are chapters for another day.</p>
<p>Until then, I hope this series helped answer a question that many developers never stop to ask:</p>
<p><strong>Why do these technologies exist in the first place?</strong></p>
<p>Because once you understand the problems they were created to solve, today's tools make a lot more sense.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why we needed React</title>
      <link>https://themissinglevel.dev/why-we-needed-react/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-we-needed-react/</guid>
      <pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate>
      <description>As web apps grew, keeping the interface in sync with the data got hard. Learn why React was created and what it changed about building user interfaces.</description>
      <category>History of the Web</category>
      <content:encoded><![CDATA[<p>Modern web applications are built with frameworks like React almost by default. Whether you're building an online store, a dashboard or a social media platform, there's a good chance React (or another component-based framework) is part of the stack.</p>
<p>But React wasn't created because developers wanted another Javascript library. It was created because the web had reached a point where existing approaches no longer scaled.</p>
<p>In the previous articles in this series, we explored how the web evolved. We saw how <a href="/why-we-needed-javascript/">Javascript</a> transformed static web pages into interactive experiences, how <a href="/why-we-needed-jquery/">jQuery</a> simplified DOM manipulation and browser compatibility, and how <a href="/why-we-needed-single-page-applications/">Single-Page Applications (SPAs)</a> made websites feel more like desktop applications.</p>
<p>Each of these technologies solved an important problem. In doing so, they also made it possible to build larger and more sophisticated applications.</p>
<p>As those applications grew, a new challenge emerged.</p>
<p>Developers no longer struggled to make pages interactive. They struggled to keep increasingly complex user interfaces predictable, consistent and easy to maintain.</p>
<p>React was created to solve that challenge.</p>
<h2 id="the-problem-wasnt-updating-the-dom">The problem wasn't updating the DOM<a class="heading-anchor" href="#the-problem-wasnt-updating-the-dom" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>One of the biggest misconceptions about React is that it was created because updating the DOM was slow. That wasn't the real problem.</p>
<p>Libraries like jQuery had already made DOM manipulation straightforward.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">$</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"#username"</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">text</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"Duke"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#B392F0">$</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"#login-button"</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">hide</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#B392F0">$</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">".notification"</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">addClass</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"success"</span><span style="color:#E1E4E8">);</span></span></code></pre></div>
<p>Updating an element was easy. The difficult part was knowing <strong>which</strong> elements needed to be updated after the application's state changed.</p>
<p>As web applications grew, different parts of the interface also started sharing the same data. A user's profile information could appear in the navigation bar, account menu, comments, chat messages and activity feed all at once.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>User updates profile</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Application state changes</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>→ Navigation bar</span></span>
<span class="line"><span>→ Account menu</span></span>
<span class="line"><span>→ Comments</span></span>
<span class="line"><span>→ Chat</span></span>
<span class="line"><span>→ Activity feed</span></span></code></pre></div>
<p>Keeping all of these views synchronized became increasingly difficult. Before React, developers had to manually update every affected part of the interface whenever the application's state changed. This approach worked for small websites, but as applications grew, it became one of the biggest sources of bugs.</p>
<p>Updating one element was easy.</p>
<p>Updating every element that depended on the same data wasn't.</p>
<h2 id="state-became-the-real-challenge">State became the real challenge<a class="heading-anchor" href="#state-became-the-real-challenge" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>But what had changed? Why did keeping the interface synchronized become so difficult?</p>
<p>Because modern web applications had to manage far more state than traditional websites ever did.</p>
<p>A page no longer displayed static content. It had to remember whether a user was logged in, what products were in their shopping cart, which notifications had been read and what information had already been entered into a form.</p>
<p>Instead of rebuilding the page after every request, applications remained active and continuously responded to user input. Every change to that state had to be reflected everywhere the data was being displayed.</p>
<p>The more state an application managed, the harder it became to keep the entire interface synchronized.</p>
<h2 id="react-introduced-a-different-way-of-thinking">React introduced a different way of thinking<a class="heading-anchor" href="#react-introduced-a-different-way-of-thinking" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Instead of telling the browser exactly how to update the page, React asked developers to describe what the interface should look like for the current application state.</p>
<p>When that state changed, React compared the new UI with the previous one and applied the DOM updates needed to reflect the change.</p>
<p>Instead of writing code like:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Find this element.</span></span>
<span class="line"><span>Change its text.</span></span>
<span class="line"><span>Hide this button.</span></span>
<span class="line"><span>Show that message.</span></span>
<span class="line"><span>Update this counter.</span></span></code></pre></div>
<p>Developers could simply think:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Current application state</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>What should the user see?</span></span></code></pre></div>
<p>This shifted the focus from manually updating the interface to describing the desired result. React handled the process of keeping the DOM synchronized, an approach known as <strong>declarative programming</strong>.</p>
<p>Rather than changing how developers manipulated the DOM, React changed how they thought about building user interfaces.</p>
<p>This may sound like a subtle difference, but it fundamentally changed how developers built applications.</p>
<p>Instead of manually updating every affected part of the page, developers described how each component should render for a given application state. React then handled translating those changes into the necessary DOM updates.</p>
<p>This reduced the amount of manual synchronization developers had to write, making large applications easier to build and maintain.</p>
<h2 id="components-made-large-applications-easier-to-manage">Components made large applications easier to manage<a class="heading-anchor" href="#components-made-large-applications-easier-to-manage" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>React also encouraged developers to split user interfaces into small, reusable components. Instead of treating an entire page as one large block of HTML and Javascript, each part of the interface became an independent building block with its own responsibility.</p>
<p>For example, an online store might be structured like this:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>&#x3C;App></span></span>
<span class="line"><span> ├── &#x3C;Navbar /></span></span>
<span class="line"><span> ├── &#x3C;SearchBar /></span></span>
<span class="line"><span> ├── &#x3C;ProductList /></span></span>
<span class="line"><span> │      └── &#x3C;ProductCard /></span></span>
<span class="line"><span> ├── &#x3C;ShoppingCart /></span></span>
<span class="line"><span> └── &#x3C;Footer /></span></span></code></pre></div>
<p>Each component described one part of the interface and could be developed, tested and maintained independently. Components could also be reused across different pages, reducing duplication and making future changes much easier.</p>
<p>This approach made large applications easier to understand. Rather than navigating a single file containing thousands of lines of code, developers could focus on one component at a time.</p>
<h2 id="the-virtual-dom-wasnt-the-main-innovation">The Virtual DOM wasn't the main innovation<a class="heading-anchor" href="#the-virtual-dom-wasnt-the-main-innovation" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This new way of building user interfaces raised an obvious question: how could React update the page efficiently without developers manually manipulating the DOM?</p>
<p>Much of the early discussion focused on the Virtual DOM. Many developers assumed this was the reason React became so successful because it appeared to solve one of the web's biggest performance concerns: avoiding unnecessary DOM updates.</p>
<p>The Virtual DOM was an important optimization, but it wasn't React's biggest innovation.</p>
<p>React's real innovation was its programming model. Developers described how the UI should look for a given application state, while React handled the process of comparing the new UI with the previous one and applying only the DOM changes that were actually needed.</p>
<p>The Virtual DOM was simply the mechanism that made this approach efficient. Even without it, the idea of building interfaces declaratively and organizing them into components would still have fundamentally changed front-end development.</p>
<h2 id="react-changed-front-end-development">React changed front-end development<a class="heading-anchor" href="#react-changed-front-end-development" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>React didn't just introduce a new API or a faster way to update the DOM. It popularized a different way of building front-end applications.</p>
<p>Concepts like components, declarative rendering and state-driven user interfaces became the foundation of modern front-end development. Today, frameworks such as Vue, Svelte, Solid and even newer versions of Angular embrace many of the same ideas, even though they implement them differently.</p>
<p>React also influenced the wider ecosystem. State management libraries, routing solutions, testing tools and design systems were all built around the component model, making it easier to develop large applications as teams and codebases grew.</p>
<p>Today, thinking in components and describing the UI as a function of state feels completely natural.</p>
<p>Before React, it wasn't.</p>
<h2 id="so-why-did-we-need-react">So why did we need React?<a class="heading-anchor" href="#so-why-did-we-need-react" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>React wasn't created because developers needed another Javascript library.</p>
<p>It was created because web applications had become too complex to manage through manual DOM updates alone.</p>
<p>By introducing a declarative, component-based approach, React made large, state-driven applications easier to build, understand and maintain. The Virtual DOM helped make this practical, but it was the programming model that changed how developers thought about user interfaces.</p>
<p>Today, those ideas have become the standard across modern front-end development.</p>
<p>React was the next step in the web's evolution.</p>
<h2 id="common-misconceptions">Common misconceptions<a class="heading-anchor" href="#common-misconceptions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="react-replaced-javascript">React replaced Javascript<a class="heading-anchor" href="#react-replaced-javascript" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. React is a Javascript library.</p>
<p>Every React application is still written using Javascript (or Typescript).</p>
<h3 id="react-made-the-dom-fast">React made the DOM fast<a class="heading-anchor" href="#react-made-the-dom-fast" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not exactly. Manipulating the DOM wasn't the biggest challenge.</p>
<p>Managing increasingly complex user interfaces was.</p>
<p>React's biggest innovation was providing a better way to describe and organize those interfaces.</p>
<h3 id="react-invented-components">React invented components<a class="heading-anchor" href="#react-invented-components" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No.</p>
<p>Component-based development had existed for many years in technologies such as Java Swing and Microsoft's .NET frameworks.</p>
<p>However, React popularized the idea and made it practical for large-scale web applications.</p>
<h3 id="you-must-use-react-to-build-modern-websites">You must use React to build modern websites<a class="heading-anchor" href="#you-must-use-react-to-build-modern-websites" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Many websites don't need React at all.</p>
<p>Traditional server-rendered applications are often simpler, faster and easier to maintain.</p>
<p>React is most valuable when building highly interactive applications with lots of shared state.</p>
<h3 id="react-replaced-jquery-overnight">React replaced jQuery overnight<a class="heading-anchor" href="#react-replaced-jquery-overnight" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not at all. For many years, both libraries coexisted.</p>
<p>Many applications even used jQuery alongside React during gradual migrations.</p>
<p>React didn't make jQuery obsolete overnight. It addressed a different set of problems as web applications became increasingly complex.</p>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="did-react-replace-javascript">Did React replace Javascript?<a class="heading-anchor" href="#did-react-replace-javascript" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No.</p>
<p>React is a Javascript library, not a replacement for the language itself. Every React application is still written using Javascript (or Typescript), and understanding Javascript fundamentals is essential for using React effectively.</p>
<p>In fact, React builds upon Javascript features such as functions, objects, modules and closures rather than replacing them.</p>
<h3 id="is-react-a-framework">Is React a framework?<a class="heading-anchor" href="#is-react-a-framework" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not officially.</p>
<p>React describes itself as a <strong>Javascript library</strong> for building user interfaces. Its primary responsibility is rendering the UI based on your application's state.</p>
<p>Unlike full frameworks, React doesn't include built-in solutions for <a href="/why-refreshing-your-single-page-app-gives-a-404/">routing</a>, state management or data fetching. Developers typically choose those tools separately, although modern frameworks like Next.js build on top of React to provide a more complete development experience.</p>
<h3 id="does-react-make-websites-faster">Does React make websites faster?<a class="heading-anchor" href="#does-react-make-websites-faster" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not necessarily.</p>
<p>React wasn't created because websites were slow.</p>
<p>It was created because building and maintaining complex user interfaces had become difficult.</p>
<p>In some cases React can improve performance by updating only the parts of the page that have changed. In other cases, a simple server-rendered website with little Javascript may actually load faster than a React application.</p>
<p>The biggest benefit of React is improving the developer experience and making large applications easier to maintain.</p>
<h3 id="why-did-react-become-so-popular">Why did React become so popular?<a class="heading-anchor" href="#why-did-react-become-so-popular" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>React solved a problem that many developers were experiencing.</p>
<p>As applications became larger, manually keeping the UI synchronized became increasingly difficult. React introduced a simpler mental model: describe what the interface should look like for the current application state, and let React handle updating the DOM.</p>
<p>Its component-based architecture, strong ecosystem and backing from Facebook also contributed to its rapid adoption.</p>
<p>Today, many of the ideas React popularized have influenced nearly every modern front-end framework.</p>
<h3 id="is-react-still-relevant-today">Is React still relevant today?<a class="heading-anchor" href="#is-react-still-relevant-today" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes.</p>
<p>React remains one of the most widely used libraries for building modern web applications, powering products used by millions of people every day.</p>
<p>The ecosystem has evolved significantly since React was first released. Features such as Hooks, Server Components and modern frameworks like Next.js have changed how React applications are built.</p>
<p>Even if another framework eventually becomes more popular, React's lasting impact isn't tied to its market share.</p>
<p>It changed how developers think about building user interfaces. Concepts such as declarative rendering, reusable components and state-driven interfaces have become standard practices across modern front-end development.</p>
<h2 id="explore-the-series">Explore the series<a class="heading-anchor" href="#explore-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>I wasn't building websites when the web was just HTML or when Javascript didn't exist. Like many developers, I learned modern frameworks first and only later became curious about how we got here.</p>
<p>This series is my way of connecting those dots. By looking back at the problems developers faced and the solutions they created, it becomes much easier to understand why today's tools exist and why the web evolved the way it did.</p>
<ul>
<li><a href="/why-we-needed-javascript/">Why we needed Javascript</a></li>
<li><a href="/why-we-needed-jquery/">Why we needed jQuery</a></li>
<li><a href="/why-we-needed-single-page-applications/">Why we needed Single-Page Applications (SPAs)</a></li>
<li><strong>Why we needed React</strong> <em>(You're reading this)</em></li>
<li><a href="/why-we-needed-node-js/">Why we needed Node.js</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why we needed Single Page Applications (SPAs)</title>
      <link>https://themissinglevel.dev/why-we-needed-single-page-applications/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-we-needed-single-page-applications/</guid>
      <pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
      <description>Before single-page applications, every click meant loading a whole new page. Learn why SPAs were created and how they changed the way web apps are built.</description>
      <category>History of the Web</category>
      <content:encoded><![CDATA[<p>So far in this series, we've followed the evolution of the web one problem at a time.</p>
<p><a href="/why-we-needed-javascript/">We started</a> with a web made entirely of static HTML pages. To create pages dynamically, servers began generating HTML on demand using technologies such as CGI.</p>
<p>That solved one problem, but browsers were still passive. Every interaction required a trip back to the server. That's why Javascript was created, allowing browsers to run code and respond immediately to user actions.</p>
<p>Later, <strong>AJAX</strong> made another major leap forward by allowing browsers to communicate with servers in the background, updating parts of a page without requiring a full page reload. We also saw how <a href="/why-we-needed-jquery/">jQuery</a> made that new way of building websites much easier by hiding browser differences behind a simple, consistent API.</p>
<p>Even then, one limitation remained.</p>
<p>Although parts of a page could now update independently, navigating to another page still meant downloading an entirely new HTML document and rebuilding the interface from scratch.</p>
<p>That limitation led to the next major evolution of the web: the Single-Page Application (SPA), a new architecture that kept the browser on the same page while Javascript updated the interface.</p>
<p><em><strong>Confession</strong>: When I first heard the term <strong>Single-Page Application</strong>, I assumed it literally meant a website made from a single HTML page. Technically... that's not completely wrong, but it's also missing the point.</em></p>
<p><em>The "single page" isn't about cramming your entire application into one enormous HTML file. It's about the browser staying on the same page while Javascript updates the interface as you navigate.</em></p>
<p><em>If you were expecting one gigantic <code>index.html</code> containing your entire application... you're definitely not the only one.</em></p>
<h2 id="the-page-reload-problem">The page reload problem<a class="heading-anchor" href="#the-page-reload-problem" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>When the web was originally designed, websites were exactly what the name suggested: pages.</p>
<p>Each click usually triggered a familiar sequence of events.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>User clicks a link</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Browser requests a new page</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Server returns an HTML document</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Browser replaces the current page</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Everything loads again</span></span></code></pre></div>
<p>For years, this was completely normal.</p>
<p>Reading a blog, browsing documentation, or shopping online didn't require a highly interactive interface. Waiting for the next page to load was simply how the web worked.</p>
<p>However, expectations began to change.</p>
<p>As web applications became more capable, users' expectations changed too.</p>
<p>People wanted to open a menu, switch between messages, browse products, or move between different sections of an application without seeing the entire page disappear and reload each time.</p>
<p>The traditional request-response model started to feel increasingly slow and disruptive.</p>
<h2 id="why-ajax-wasnt-enough">Why AJAX wasn't enough<a class="heading-anchor" href="#why-ajax-wasnt-enough" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>AJAX was a huge step forward. Instead of refreshing the entire page, browsers could request only the data they needed and update a small part of the interface. Search suggestions could appear as you typed, shopping carts could update instantly, new chat messages could arrive automatically, and comments could be posted without interrupting what you were reading.</p>
<p>But AJAX solved only part of the problem. It improved individual interactions without changing how websites were fundamentally built. Each page was still a separate HTML document served by the server.</p>
<p>In a traditional <strong>Multi-Page Application (MPA)</strong>, clicking a navigation link typically requested a completely new HTML document. The browser discarded the current page, downloaded a new one, reloaded the application's Javascript and CSS, and started everything again from scratch.</p>
<p>Developers had made websites far more interactive, but the web was still built around navigating from one page to another. As users began expecting web applications to behave more like desktop software, that traditional page-by-page model started to show its age.</p>
<h2 id="single-page-applications-arrive">Single-Page Applications arrive<a class="heading-anchor" href="#single-page-applications-arrive" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>A Single-Page Application (SPA) can contain dozens or even hundreds of different screens. The "single page" refers to the browser loading one HTML document initially, while Javascript takes over navigation using <strong><a href="/why-refreshing-your-single-page-app-gives-a-404/">client-side routing</a></strong> and updates the interface <strong>without performing a full page reload</strong>.</p>
<p>Instead of requesting a new page for every navigation, the browser keeps the application running and asks the server only for the data it needs. That matters because <a href="/what-happens-after-you-press-enter/">a full page load</a> is never free, and the traditional model paid that cost again on every single click.</p>
<p>The difference is easier to understand visually.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span># Traditional Multi-Page Application (MPA)</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Click</span></span>
<span class="line"><span>   ↓</span></span>
<span class="line"><span>Server returns a new HTML page</span></span>
<span class="line"><span>   ↓</span></span>
<span class="line"><span>Browser replaces everything</span></span>
<span class="line"><span></span></span>
<span class="line"><span>---</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Single-Page Application (SPA)</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Application is already loaded</span></span>
<span class="line"><span>          ↓</span></span>
<span class="line"><span>User clicks a link</span></span>
<span class="line"><span>          ↓</span></span>
<span class="line"><span>Javascript updates the interface</span></span>
<span class="line"><span>          ↓</span></span>
<span class="line"><span>Server returns only the required data (if needed)</span></span></code></pre></div>
<p>From the user's perspective, the experience became dramatically smoother. Navigation felt almost instant, animations remained uninterrupted, and the application could preserve its state while moving between different screens.</p>
<p>For the first time, websites no longer behaved like a collection of separate pages. They felt like a single, continuous application running inside the browser.</p>
<p>This architectural shift paved the way for frameworks such as React, Angular, and Vue, which embraced this new model and made it practical to build increasingly complex web applications.</p>
<h3 id="the-trade-offs">The trade-offs<a class="heading-anchor" href="#the-trade-offs" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Like every major shift in web development, Single-Page Applications solved one set of problems while introducing new ones.</p>
<p>Because much more of the application now lived in the browser, the initial download often became significantly larger. Instead of receiving a small HTML page for each request, users typically downloaded a large Javascript bundle before the application became fully interactive.</p>
<p>Building SPAs also increased complexity for developers. Client-side routing, state management, authentication, browser history, and data synchronization all became responsibilities of the application instead of the server. Moving those concerns into the browser is a user experience win, but it's worth being clear that it moves the code without moving the security, because <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a> no matter how much of the application now runs there.</p>
<p>Search engine optimization (SEO) was another challenge in the early days of SPAs. Many applications initially sent the browser a nearly empty HTML document containing little more than a root element, with Javascript responsible for fetching data and rendering the entire interface afterwards.</p>
<p>Early search engines often indexed only that initial HTML, meaning they couldn't reliably see the content users eventually saw in their browsers. As a result, some SPA websites struggled to appear in search results.</p>
<p>Over time, both search engines and web frameworks improved. Techniques such as <strong>Server-Side Rendering (SSR)</strong> and <strong>Static Site Generation (SSG)</strong> made it possible to send fully rendered HTML from the start, allowing search engines to crawl and index pages much more reliably.</p>
<h2 id="what-came-after-single-page-applications">What came after single-page applications?<a class="heading-anchor" href="#what-came-after-single-page-applications" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Single-Page Applications changed how websites were built, but they didn't solve every problem.</p>
<p>As applications grew larger, managing hundreds of components, events, and DOM updates with libraries like jQuery became increasingly difficult. Codebases became harder to organize, maintain, and reason about. Avoiding page reloads was no longer the biggest challenge. Building and maintaining large client-side applications was.</p>
<p>That challenge led to the rise of modern frontend frameworks, beginning with React, which introduced a new way of thinking about building user interfaces.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>Single-Page Applications changed the web by loading a single HTML document and updating the interface with Javascript instead of reloading entire pages.</li>
<li>While AJAX made individual interactions more dynamic, SPAs rethought the overall architecture of web applications.</li>
<li>The complexity of building large SPAs eventually led to the development of frameworks like React, which provided better ways to organize and manage growing applications.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="what-is-a-single-page-application">What is a Single-Page Application?<a class="heading-anchor" href="#what-is-a-single-page-application" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A Single-Page Application (SPA) is a web application that loads a single HTML document initially and uses Javascript to update the interface as users interact with it. Instead of requesting a new page for every navigation, the browser updates the existing page and fetches only the data it needs.</p>
<h3 id="are-spas-still-popular">Are SPAs still popular?<a class="heading-anchor" href="#are-spas-still-popular" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. Single-Page Applications are still widely used for applications where users spend a lot of time interacting with the interface, such as email clients, project management tools, messaging applications, dashboards, and online editors. Although developers now have more architectural choices, the SPA model continues to power many modern web applications.</p>
<h3 id="can-single-page-applications-be-seo-friendly">Can Single-Page Applications be SEO friendly?<a class="heading-anchor" href="#can-single-page-applications-be-seo-friendly" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not necessarily. Early SPAs often struggled with SEO because search engines had difficulty indexing content rendered entirely with Javascript. Today, frameworks like Next.js support server-side rendering and static site generation, allowing developers to build SPA-like experiences while maintaining excellent SEO.</p>
<h3 id="is-react-a-single-page-application">Is React a Single-Page Application?<a class="heading-anchor" href="#is-react-a-single-page-application" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. React is a Javascript library for building user interfaces, not an application architecture.</p>
<p>However, React is commonly used to build Single-Page Applications, which is why the two concepts are often associated. React can also be used to build server-rendered websites, static sites, or individual interactive components within traditional multi-page applications.</p>
<h2 id="explore-the-series">Explore the series<a class="heading-anchor" href="#explore-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>I wasn't building websites when the web was just HTML or when Javascript didn't exist. Like many developers, I learned modern frameworks first and only later became curious about how we got here.</p>
<p>This series is my way of connecting those dots. By looking back at the problems developers faced and the solutions they created, it becomes much easier to understand why today's tools exist and why the web evolved the way it did.</p>
<ul>
<li><a href="/why-we-needed-javascript/">Why we needed Javascript</a></li>
<li><a href="/why-we-needed-jquery/">Why we needed jQuery</a></li>
<li><strong>Why we needed Single-Page Applications (SPAs)</strong> <em>(You're reading this)</em></li>
<li><a href="/why-we-needed-react/">Why we needed React</a></li>
<li><a href="/why-we-needed-node-js/">Why we needed Node.js</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why we needed jQuery</title>
      <link>https://themissinglevel.dev/why-we-needed-jquery/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-we-needed-jquery/</guid>
      <pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate>
      <description>Before browsers behaved the same way, building interactive websites was painful. Learn why jQuery appeared, what it fixed, and why it later declined.</description>
      <category>History of the Web</category>
      <content:encoded><![CDATA[<p>In the previous article, <a href="/why-we-needed-javascript/">Why we needed Javascript</a>, we saw how browsers gained the ability to run code, transforming static web pages into interactive experiences.</p>
<p>That solved one of the web's biggest limitations, but it also exposed a new problem.</p>
<p>As developers started building richer websites, they discovered that the same Javascript code often behaved differently across browsers. Writing Javascript had become possible. Writing <strong>Javascript that worked everywhere</strong> was another challenge entirely.</p>
<h2 id="the-problem-browsers-didnt-agree">The problem: browsers didn't agree<a class="heading-anchor" href="#the-problem-browsers-didnt-agree" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The late 1990s and early 2000s were an exciting time for the web. Javascript was becoming more popular, websites were growing increasingly interactive, and browsers were competing aggressively for users.</p>
<p>Unfortunately, they weren't always competing in the same direction.</p>
<p>There wasn't yet a fully consistent standard for how browsers should behave. Internet Explorer, Netscape Navigator, Opera, Safari, and later Firefox often introduced their own APIs, features, and quirks.</p>
<p>As a result, developers frequently discovered that code working perfectly in one browser would fail in another.</p>
<p>For example, attaching a click event wasn't always done the same way.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D">// Internet Explorer</span></span>
<span class="line"><span style="color:#E1E4E8">element.</span><span style="color:#B392F0">attachEvent</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"onclick"</span><span style="color:#E1E4E8">, handler);</span></span></code></pre></div>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D">// Most other browsers</span></span>
<span class="line"><span style="color:#E1E4E8">element.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"click"</span><span style="color:#E1E4E8">, handler);</span></span></code></pre></div>
<p>Even selecting elements, handling events, or reading CSS properties could require different code depending on the browser.</p>
<p>Supporting all of those browsers quickly became exhausting. Projects filled with browser-specific conditions, compatibility fixes, and workarounds, leaving developers spending as much time fighting browser inconsistencies as building new features.</p>
<p>The larger an application became, the harder those differences were to maintain.</p>
<p>Developers needed a better solution.</p>
<h2 id="jquery-arrives">jQuery arrives<a class="heading-anchor" href="#jquery-arrives" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>In 2006, <strong>John Resig</strong> introduced <strong>jQuery</strong> with a simple goal:</p>
<blockquote>
<p>Write less, do more.</p>
</blockquote>
<p>Instead of forcing developers to remember dozens of browser differences, jQuery provided a simple, consistent API that worked across all major browsers, making cross-browser development dramatically easier.</p>
<p>It didn't replace Javascript. Instead, it provided a simpler and more consistent way to work with it.</p>
<p>Conceptually, jQuery sat between your code and the browser, hiding many of the differences between browser implementations.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>        Your code</span></span>
<span class="line"><span>            ↓</span></span>
<span class="line"><span>          jQuery</span></span>
<span class="line"><span>   ┌────────┼────────┐</span></span>
<span class="line"><span>   ↓        ↓        ↓</span></span>
<span class="line"><span>   IE    Firefox   Safari</span></span></code></pre></div>
<p>Much of that convenience came from simplifying DOM manipulation, event handling, animations, and AJAX into a small, consistent set of functions.</p>
<p>Let's look at what that meant in practice.</p>
<p>One of the biggest reasons developers embraced jQuery was how much simpler common tasks became.</p>
<h3 id="before-jquery">Before jQuery<a class="heading-anchor" href="#before-jquery" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Selecting an element:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"menu"</span><span style="color:#E1E4E8">);</span></span></code></pre></div>
<p>Adding a click event:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"button"</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"click"</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8"> () {</span></span>
<span class="line"><span style="color:#B392F0">    alert</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"Clicked!"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span></code></pre></div>
<h3 id="after-jquery">After jQuery<a class="heading-anchor" href="#after-jquery" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Selecting an element:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">$</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"#menu"</span><span style="color:#E1E4E8">);</span></span></code></pre></div>
<p>Adding a click event:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">$</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"#button"</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">click</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">function</span><span style="color:#E1E4E8"> () {</span></span>
<span class="line"><span style="color:#B392F0">  alert</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"Clicked!"</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span></code></pre></div>
<p>The difference looks small in these examples, but across hundreds or thousands of lines of code, the productivity gains quickly added up.</p>
<p>The shorter syntax made code easier to read, but jQuery's biggest advantage wasn't saving a few keystrokes.</p>
<p>By hiding browser-specific differences behind a consistent API, developers could write code once and spend far less time debugging compatibility issues.</p>
<h2 id="plugins-changed-everything">Plugins changed everything<a class="heading-anchor" href="#plugins-changed-everything" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>As jQuery grew in popularity, it became much more than a compatibility library.</p>
<p>Need form validation? There was a plugin.</p>
<p>Need a date picker? There was a plugin.</p>
<p>Need a carousel, lightbox, drag-and-drop interface, or animation? There was a plugin.</p>
<p>An enormous ecosystem grew around jQuery, allowing developers to add sophisticated functionality with only a few lines of code instead of building everything from scratch.</p>
<p>Combined with excellent documentation and a vibrant community, jQuery quickly became one of the most influential libraries in the history of web development.</p>
<p>At its peak, it powered the vast majority of websites on the internet, making it almost impossible to build for the web without encountering jQuery.</p>
<h2 id="why-did-jquery-decline">Why did jQuery decline?<a class="heading-anchor" href="#why-did-jquery-decline" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Ironically, jQuery became so successful that browsers eventually adopted many of the APIs and patterns developers had come to rely on.</p>
<p>Modern browsers became far more consistent, while Javascript and the browser platform continued to evolve.</p>
<p>Many of the conveniences that once made jQuery essential became part of the web itself. Features such as <code>querySelector()</code>, <code>fetch()</code>, <code>classList</code>, and improved event handling allowed developers to write simpler, cross-browser code without relying on a library.</p>
<p>At the same time, frameworks such as React, Angular, and Vue emerged, solving problems that jQuery had never been designed to address, such as building and managing large, interactive applications.</p>
<p>Today, jQuery remains an important part of web history. It solved a real problem at exactly the right time and helped shape the modern web, even if many of its original responsibilities are now handled directly by browsers and modern Javascript.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>jQuery solved one of the biggest challenges of early web development by providing a consistent API across browsers, allowing developers to write less compatibility code.</li>
<li>Its plugin ecosystem and simple syntax dramatically accelerated development, making jQuery one of the most influential libraries in web history.</li>
<li>As browsers standardized and Javascript evolved, many of jQuery's core features became built into the web platform, paving the way for modern frameworks to solve a new generation of problems.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="was-jquery-a-programming-language">Was jQuery a programming language?<a class="heading-anchor" href="#was-jquery-a-programming-language" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. jQuery is a Javascript library, not a programming language. It provides a collection of functions that make common Javascript tasks easier and more consistent across different browsers.</p>
<h3 id="is-jquery-still-used">Is jQuery still used?<a class="heading-anchor" href="#is-jquery-still-used" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. Although many new projects no longer depend on it, jQuery is still used by millions of websites, especially older applications and content management systems like WordPress. It also continues to be actively maintained.</p>
<h3 id="should-you-learn-jquery-today">Should you learn jQuery today?<a class="heading-anchor" href="#should-you-learn-jquery-today" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>If you're learning web development, focus on modern Javascript first. Today's browsers support many features that once required jQuery, and modern frameworks often don't rely on it. However, understanding jQuery is still valuable if you maintain legacy applications, work with existing codebases, or develop for platforms that still use it.</p>
<h3 id="does-wordpress-still-use-jquery">Does WordPress still use jQuery?<a class="heading-anchor" href="#does-wordpress-still-use-jquery" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. WordPress still includes jQuery, and many themes and plugins rely on it for interactive features such as sliders, menus, form validation, and pop-ups.</p>
<p>That said, modern WordPress development is gradually moving away from jQuery. The Block Editor (Gutenberg) is built with React, and many new themes and plugins now prefer modern Javascript APIs instead of depending on jQuery.</p>
<p>If you're maintaining an existing WordPress site, you'll almost certainly encounter jQuery. However, if you're building new functionality today, modern Javascript is generally the recommended approach.</p>
<h2 id="explore-the-series">Explore the series<a class="heading-anchor" href="#explore-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>I wasn't building websites when the web was just HTML or when Javascript didn't exist. Like many developers, I learned modern frameworks first and only later became curious about how we got here.</p>
<p>This series is my way of connecting those dots. By looking back at the problems developers faced and the solutions they created, it becomes much easier to understand why today's tools exist and why the web evolved the way it did.</p>
<ul>
<li><a href="/why-we-needed-javascript/">Why we needed Javascript</a></li>
<li><strong>Why we needed jQuery</strong> <em>(You're reading this)</em></li>
<li><a href="/why-we-needed-single-page-applications/">Why we needed Single-Page Applications (SPAs)</a></li>
<li><a href="/why-we-needed-react/">Why we needed React</a></li>
<li><a href="/why-we-needed-node-js/">Why we needed Node.js</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why we needed Javascript</title>
      <link>https://themissinglevel.dev/why-we-needed-javascript/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-we-needed-javascript/</guid>
      <pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate>
      <description>Before Javascript, websites were little more than static documents. Learn why Javascript was created and the problems it was built to solve.</description>
      <category>History of the Web</category>
      <content:encoded><![CDATA[<p>Javascript wasn't created because developers wanted another programming language. It was created because the web had a problem:</p>
<blockquote>
<p>HTML could display documents, but it couldn't react to users.</p>
</blockquote>
<h2 id="the-web-before-javascript">The web before Javascript<a class="heading-anchor" href="#the-web-before-javascript" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>When the World Wide Web first became popular in the early 1990s, websites looked very different from what we use today. They were mostly collections of linked documents. You could open a page, read its content, click a link, and move to another page. For sharing information, this worked remarkably well.</p>
<p>At the time, HTML was exactly what it was designed to be: <strong>a markup language for describing documents</strong>. It could define headings, paragraphs, images, tables, and links, allowing browsers to display content consistently across different computers.</p>
<p>The lack of interactivity wasn't considered a limitation because the web wasn't originally designed to run applications.</p>
<blockquote>
<p>The Web was designed to publish and browse documents</p>
</blockquote>
<p>As more people started using the internet, however, developers began imagining websites that could do much more than display information. That change exposed a limitation that HTML alone could not solve.</p>
<h2 id="the-problem-static-pages">The problem: static pages<a class="heading-anchor" href="#the-problem-static-pages" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>As websites became more interactive, developers wanted to do more than simply display static information. They wanted pages that could respond immediately to user actions, validate forms, and react without constantly loading a new page.</p>
<p>HTML wasn't designed for any of that. Once a page had loaded, its content couldn't change unless the browser requested a new page from the server.</p>
<p>Imagine filling out a registration form:</p>
<ol>
<li>You enter your details.</li>
<li>You click <strong>Submit</strong>.</li>
<li><a href="/what-happens-after-you-press-enter/">The browser sends your data to the server.</a></li>
<li>The server processes the request.</li>
<li>The browser loads an entirely new page.</li>
<li>Only then do you discover you forgot to enter your email address.</li>
</ol>
<p>Just to tell you that one field was missing, the browser had to reload the entire page. <strong>There was no way to validate your input</strong> instantly while you were typing.</p>
<p>Adding an item to a shopping cart, changing a quantity, logging in, and many other common tasks all meant sending another request to the server and replacing the current page. Unlike today, back then <strong>only a full page reload</strong> could update what the user saw.</p>
<p>As a result, every interaction interrupted what you were doing.</p>
<p>Developers started to realize they needed a way for websites to react immediately to user actions instead of replacing the entire page every time something changed.</p>
<p>That need eventually led to the creation of <strong>Javascript</strong>.</p>
<h2 id="the-first-solution-cgi-and-server-side-scripts">The first solution: CGI and server-side scripts<a class="heading-anchor" href="#the-first-solution-cgi-and-server-side-scripts" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Before we look at how Javascript solved this problem in the browser, it's worth understanding how developers first made websites dynamic on the server.</p>
<p>Early websites were often just collections of HTML files stored on the server.</p>
<p>When someone visited the home page, the server returned <code>index.html</code>. When they visited the contact page, it returned <code>contact.html</code>. Each URL simply pointed to a different file.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>/          → index.html</span></span>
<span class="line"><span>/contact   → contact.html</span></span>
<span class="line"><span>/about     → about.html</span></span></code></pre></div>
<p>That worked well for static websites, but it quickly became impractical. Imagine a website with a search box. It couldn't possibly have a separate HTML file for every search someone might type. The same problem applied to logged-in users and form submissions. The server needed a way to create the page only after it knew what the user had searched for or submitted.</p>
<p>Instead of returning a pre-existing file, the server began running programs that generated the HTML for each request. This allowed websites to produce different pages depending on who the user was and what they were trying to do.</p>
<h3 id="cgi---common-gateway-interface">CGI - Common Gateway Interface<a class="heading-anchor" href="#cgi---common-gateway-interface" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>One of the earliest ways to do this was through <strong>CGI (Common Gateway Interface)</strong>.</p>
<p>The name sounds more complicated than it really is.</p>
<ul>
<li><strong>Common</strong> because it was a standard that any web server and any program could follow.</li>
<li><strong>Gateway</strong> because it acted as a bridge between the web server and external programs.</li>
<li><strong>Interface</strong> because it defined how the server and those programs communicated.</li>
</ul>
<p>In simple terms, CGI gave web servers a standard way to run a program that generated a web page instead of simply returning a static HTML file.</p>
<p><strong>This was a major step forward.</strong></p>
<p>Later, server-side technologies such as PHP, ASP, JSP, and others made this approach much easier to build. The idea, however, remained the same: every interaction happened on the server.</p>
<p>If a user clicked a button, submitted a form, or searched for something, the browser still had to send a request to the server. The server generated a new HTML page, and the browser replaced the current one with the response.</p>
<p>The web had become <strong>dynamic</strong>, but it still wasn't interactive.</p>
<blockquote>
<p><strong>Note:</strong> If you've worked with frameworks like Next.js, you've already used this concept. Modern Server-Side Rendering (SSR) follows the same fundamental idea: the server executes code, generates HTML, and sends it to the browser. Technologies such as CGI, PHP, and ASP were earlier ways of achieving the same goal.</p>
</blockquote>
<h2 id="why-that-wasnt-enough">Why that wasn't enough<a class="heading-anchor" href="#why-that-wasnt-enough" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Generating pages on the server solved many problems, but one limitation remained: <strong>the browser still couldn't execute its own logic</strong>.</p>
<p>Even simple tasks required talking to the server first. A website couldn't instantly check whether a required field was empty, calculate a total as you typed, or show and hide parts of the page without requesting another page from the server.</p>
<p>As websites grew more sophisticated, this model started to feel restrictive. Developers wanted pages that could react immediately to user input instead of waiting for a round trip to the server every time something happened.</p>
<p>The server was excellent at processing data and storing information, but not every interaction needed its help.</p>
<p>For example, if a required field was left empty or an email address was clearly invalid, the browser didn't need to ask the server to figure that out. It could check those things immediately.</p>
<p>Some logic belonged much closer to the user.</p>
<p>That raised an important question:</p>
<blockquote>
<p>What if the browser could run code too?</p>
</blockquote>
<h2 id="netscapes-idea-a-language-for-the-browser">Netscape's idea: a language for the browser<a class="heading-anchor" href="#netscapes-idea-a-language-for-the-browser" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>In the mid-1990s, the web was growing at an incredible pace. One of the companies leading that growth was Netscape, whose browser had quickly become one of the most popular ways to access the web.</p>
<p>Netscape saw where the web was heading. Websites were no longer just collections of documents; they were slowly becoming applications. To support that future, browsers needed more than HTML. They needed a way to react to user input without constantly relying on the server.</p>
<p>The idea was surprisingly simple: instead of sending every interaction across the internet, let the browser execute small pieces of code directly on the user's computer.</p>
<p>That code could respond to clicks, validate forms before they were submitted, update parts of a page, and create a smoother, more responsive experience.</p>
<p>It wouldn't replace server-side programming. Instead, it would complement it by handling interactions that didn't require a trip to the server.</p>
<h2 id="javascript-is-born">Javascript is born<a class="heading-anchor" href="#javascript-is-born" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>To make that vision possible, Netscape asked engineer <strong>Brendan Eich</strong> to create a scripting language that could run directly inside the browser. In 1995, he developed the first version of what would eventually become Javascript.</p>
<p>The language was originally called <strong>LiveScript</strong>, but Netscape renamed it <strong>Javascript</strong> shortly before its release. The new name reflected the popularity of Java at the time, even though the two languages were designed for different purposes.</p>
<p>Ironically, that naming decision still causes confusion today. Many people learning programming for the first time assume Java and Javascript are closely related, when in reality they are completely different languages that happen to share part of their name.</p>
<p>Unlike languages designed to build desktop applications or server software, this new language focused on making web pages interactive. It could respond to events, modify the HTML displayed in the browser, and react instantly to user actions.</p>
<p>For the first time, web pages were no longer limited to displaying information. They could react while the user was still on the page.</p>
<p>This approach completely changed the direction of the web. What started as a language for adding small interactive features would eventually become one of the most important programming languages in the world.</p>
<h2 id="what-javascript-could-do-that-html-couldnt">What Javascript could do that HTML couldn't<a class="heading-anchor" href="#what-javascript-could-do-that-html-couldnt" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Javascript gave browsers something they had never had before: the ability to run code after a page had loaded.</p>
<p>Instead of asking the server to handle every interaction, the browser could now respond immediately to what the user was doing. This made websites feel faster, smoother, and far more interactive.</p>
<p>Some of the things Javascript made possible included:</p>
<ul>
<li>Validating forms before they were submitted.</li>
<li>Responding to button clicks instantly.</li>
<li>Showing or hiding parts of a page.</li>
<li>Updating content without rebuilding the entire page.</li>
<li>Creating animations and visual effects.</li>
<li>Reacting to keyboard and mouse events.</li>
</ul>
<p>This didn't eliminate the need for servers. Browsers still relied on them to store data, authenticate users, and generate content. Instead, Javascript allowed the browser and the server to share the work, with each handling the tasks it was best suited for.</p>
<p>Although these features may seem ordinary today, in the mid-1990s they completely changed what developers expected from the web.</p>
<h2 id="the-browser-wars">The browser wars<a class="heading-anchor" href="#the-browser-wars" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The first versions of Javascript were intentionally small. The goal wasn't to build large applications, it was to add simple interactions to web pages.</p>
<p>As developers discovered new ways to use the language, websites became increasingly dynamic. At the same time, browsers competed aggressively by introducing their own features and behaviors.</p>
<p>Unfortunately, those features weren't always compatible.</p>
<p>Netscape Navigator and Internet Explorer often implemented the same ideas differently. Developers frequently had to write browser-specific code, turning cross-browser compatibility into one of the biggest challenges of early web development.</p>
<p>Code that worked perfectly in one browser could fail completely in another. Developers often had to write separate code paths, detect the user's browser, or accept that some features simply wouldn't work everywhere.</p>
<p>The language itself also evolved quickly. Some design decisions made sense for small scripts but became awkward as applications grew larger. Those early compromises are still visible in parts of the language today.</p>
<p>Despite these challenges, developers continued pushing the browser further than anyone had originally imagined.</p>
<h2 id="ajax-changes-everything">AJAX changes everything<a class="heading-anchor" href="#ajax-changes-everything" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>By the early 2000s, websites were becoming increasingly interactive. Javascript was handling more work inside the browser, but developers kept running into the same limitation.</p>
<p>Whenever fresh data was needed, the browser still had to navigate to a new page. Updating a shopping cart, checking for new messages, or refreshing a list of products usually meant replacing the current page with a new one.
That experience started to feel increasingly outdated. Developers wanted browsers to fetch new information without forcing users to leave the page they were already using.</p>
<p>That idea eventually became known as <strong>AJAX</strong>, short for <strong>Asynchronous Javascript and XML</strong>.</p>
<p><em>Why <strong>XML?</strong> When AJAX became popular, websites commonly exchanged data using XML, which is where the acronym comes from.</em></p>
<p>AJAX allowed browsers to communicate with the server in the background while keeping the current page exactly as it was. Instead of replacing the entire page, websites could request only the data they needed and update a small part of the interface.</p>
<p>This made entirely new experiences possible.</p>
<p>Instead of waiting for a full page reload after every action, websites could:</p>
<ul>
<li>Load new messages automatically.</li>
<li>Update shopping carts instantly.</li>
<li>Search while the user was typing.</li>
</ul>
<p>Although modern applications rarely exchange XML anymore, the underlying idea remains the same. Today's <code>fetch()</code> API follows the same principle: communicating with the server without replacing the current page.</p>
<p>AJAX marked another major step in the evolution of the web. By allowing Javascript to communicate with the server in the background, websites became faster, smoother, and far more interactive.</p>
<h2 id="how-javascript-evolved-after-ajax">How Javascript evolved after AJAX<a class="heading-anchor" href="#how-javascript-evolved-after-ajax" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The arrival of AJAX marked another turning point.</p>
<p>Developers had proven that browsers could do far more than simply display documents. Over the years that followed, Javascript evolved from a small scripting language into the foundation of modern web applications.</p>
<p>Libraries such as jQuery simplified browser compatibility. Later, frameworks like Angular, React, and Vue made it possible to build complex applications that ran almost entirely inside the browser.</p>
<p>The language itself continued to evolve. New versions introduced features such as classes, modules, promises, arrow functions, and async/await, making Javascript easier to write and maintain.</p>
<p>Eventually, Javascript expanded beyond the browser.</p>
<p>With the introduction of <a href="/why-we-needed-node-js/">Node.js</a>, developers could use the same language on both the client and the server. Instead of switching between multiple programming languages, teams could build entire applications using Javascript across the full stack.</p>
<p>What started as a lightweight scripting language had become one of the foundations of modern software development.</p>
<h2 id="the-trade-offs-we-still-live-with">The trade-offs we still live with<a class="heading-anchor" href="#the-trade-offs-we-still-live-with" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Javascript solved one of the biggest limitations of the early web, but every solution comes with trade offs.</p>
<p>As browsers became more capable, Javascript grew from a small scripting language into one of the world's most widely used programming languages. Along the way, the web became far more powerful but also far more complex.</p>
<p>Modern applications often depend on frameworks, build tools, package managers, and thousands of lines of Javascript before they become fully interactive. At the same time, the language has remained remarkably backward compatible. Features introduced decades ago still exist because removing them would break countless websites.</p>
<p>Those trade-offs have allowed the web to evolve without leaving older websites behind. While Javascript isn't perfect, it's difficult to imagine today's web without it.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>The early web was designed to publish and browse documents, not run applications. HTML could describe content, but it couldn't respond to user actions after a page had loaded.</li>
<li>Javascript brought programming directly into the browser, making websites interactive.</li>
<li>AJAX allowed browsers to communicate with servers without replacing the entire page.</li>
<li>Over time, Javascript evolved from a simple scripting language into a platform used across browsers, servers, desktop applications, and more.</li>
</ul>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="why-wasnt-html-enough">Why wasn't HTML enough?<a class="heading-anchor" href="#why-wasnt-html-enough" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>HTML was designed to describe and structure documents. It could display text, images, links, and forms, but it couldn't execute logic or respond to user actions after a page had loaded. Javascript filled that gap by allowing browsers to run code.</p>
<h3 id="could-websites-work-without-javascript-today">Could websites work without Javascript today?<a class="heading-anchor" href="#could-websites-work-without-javascript-today" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes. A website can still function without Javascript, especially if it mainly displays content. However, many modern features such as instant form validation, interactive maps, live search, drag-and-drop interfaces, and single page applications (SPA) depend on Javascript.</p>
<h3 id="is-javascript-the-same-as-java">Is Javascript the same as Java?<a class="heading-anchor" href="#is-javascript-the-same-as-java" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Despite their similar names, Javascript and Java are different programming languages with different designs and use cases. Netscape chose the name "Javascript" largely for marketing reasons because Java was extremely popular at the time.</p>
<h3 id="why-does-javascript-have-so-many-strange-behaviors">Why does Javascript have so many strange behaviors?<a class="heading-anchor" href="#why-does-javascript-have-so-many-strange-behaviors" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Many of Javascript's unusual behaviors are the result of design decisions made in its early years. Rather than breaking millions of existing websites, the language has remained highly backward compatible, preserving behaviors that developers still encounter today.</p>
<h3 id="what-would-the-web-look-like-if-javascript-had-never-existed">What would the web look like if Javascript had never existed?<a class="heading-anchor" href="#what-would-the-web-look-like-if-javascript-had-never-existed" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Without Javascript, websites would likely behave much like they did in the early days of the web. Most interactions would require the browser to request a new page from the server, and many of the responsive experiences we take for granted today wouldn't exist. While another browser language might eventually have emerged, the modern web would almost certainly have evolved very differently.</p>
<h2 id="explore-the-series">Explore the series<a class="heading-anchor" href="#explore-the-series" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>I wasn't building websites when the web was just HTML or when Javascript didn't exist. Like many developers, I learned modern frameworks first and only later became curious about how we got here.</p>
<p>This series is my way of connecting those dots. By looking back at the problems developers faced and the solutions they created, it becomes much easier to understand why today's tools exist and why the web evolved the way it did.</p>
<ul>
<li><strong>Why we needed Javascript</strong> <em>(You're reading this)</em></li>
<li><a href="/why-we-needed-jquery/">Why we needed jQuery</a></li>
<li><a href="/why-we-needed-single-page-applications/">Why we needed Single-Page Applications (SPAs)</a></li>
<li><a href="/why-we-needed-react/">Why we needed React</a></li>
<li><a href="/why-we-needed-node-js/">Why we needed Node.js</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>What happens during a DNS lookup? A step-by-step guide</title>
      <link>https://themissinglevel.dev/what-happens-during-a-dns-lookup-a-step-by-step-guide/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/what-happens-during-a-dns-lookup-a-step-by-step-guide/</guid>
      <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
      <description>Before your browser can connect, it must find the site's IP address. Learn how a DNS lookup works step by step, from browser cache to authoritative server.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>When you open your browser and type:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>https://themissinglevel.dev</span></span></code></pre></div>
<p>A few moments later, the website appears.</p>
<p>It feels almost instant, but your browser couldn't possibly know where to find the website on its own. The internet doesn't route traffic using names like <code>themissinglevel.dev</code>. It routes traffic using IP addresses such as <code>203.0.113.42</code> or IPv6 addresses like <code>2001:db8::42</code>.</p>
<p>Without first finding that IP address through a DNS lookup, your browser wouldn't know where to send the request.</p>
<p>So before anything else can happen, your browser has to answer a simple question:</p>
<blockquote>
<p><strong>Which computer should I connect to?</strong></p>
</blockquote>
<p>Think of it like calling someone on your phone. You tap a contact named <strong>Joe</strong> instead of memorizing a phone number. Before the call can be placed, your phone looks up the number associated with that contact.</p>
<p>Your browser works in much the same way. Before it can connect to a website, it performs a DNS lookup to find the IP address associated with the domain.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>      You type a domain</span></span>
<span class="line"><span></span></span>
<span class="line"><span>      themissinglevel.dev</span></span>
<span class="line"><span>              ↓</span></span>
<span class="line"><span>      DNS finds the IP</span></span>
<span class="line"><span>              ↓</span></span>
<span class="line"><span>         203.0.113.42</span></span>
<span class="line"><span>              ↓</span></span>
<span class="line"><span>Browser sends the request to that IP</span></span></code></pre></div>
<p>Without DNS, the browser wouldn't know where to connect, so the request could never reach the website.</p>
<p>Now let's see what actually happens when your browser looks up a domain name.</p>
<h2 id="what-exactly-is-dns">What exactly is DNS?<a class="heading-anchor" href="#what-exactly-is-dns" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Although DNS (Domain Name System) often feels like a single service, it isn't. It's <strong>a distributed system</strong> made up of thousands of <a href="/what-is-a-server-really/">DNS servers</a> around the world. Each DNS server is responsible for only a small part of the internet's namespace, and together they can resolve millions of domain names.</p>
<p>This distributed design makes DNS scalable, resilient, and fast. If every domain on the internet had to be stored in one central database, the system would quickly become impossible to manage. Instead, responsibility is shared across many DNS servers. Each one manages a different part of the DNS hierarchy and works together with the others to answer queries.</p>
<p>Because each server only manages a small part of the internet's namespace, DNS can continue to grow without relying on one massive central database.</p>
<h2 id="how-a-dns-lookup-works-step-by-step">How a DNS lookup works, step by step<a class="heading-anchor" href="#how-a-dns-lookup-works-step-by-step" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Although a DNS lookup can involve several servers across the internet, the browser always starts with the fastest option: <strong>checking whether it already knows the answer.</strong></p>
<h3 id="step-1-the-browser-checks-its-dns-cache">Step 1: The browser checks its DNS cache<a class="heading-anchor" href="#step-1-the-browser-checks-its-dns-cache" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The browser's first question isn't <em>"Which DNS server should I contact?"</em></p>
<p>It's:</p>
<blockquote>
<p><strong>"Have I looked up this domain recently?"</strong></p>
</blockquote>
<p>Modern web browsers maintain a small DNS cache in memory to speed up future DNS lookups. If you've recently visited the same website, the browser may already have the corresponding IP address stored locally.</p>
<p>For example, if you visited <code>themissinglevel.dev</code> a few minutes ago, the browser might already know that it resolves to <code>203.0.113.42</code>.</p>
<p>In that case, there's no need to perform another DNS lookup. The browser can immediately begin establishing a connection with the server, making the page load a little faster and avoiding unnecessary network traffic.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span></span></span>
<span class="line"><span>            User enters</span></span>
<span class="line"><span>       themissinglevel.dev</span></span>
<span class="line"><span>                ↓</span></span>
<span class="line"><span>      ┌─────────────────────┐</span></span>
<span class="line"><span>      │ Browser DNS Cache   │</span></span>
<span class="line"><span>      └─────────────────────┘</span></span>
<span class="line"><span>           │           │</span></span>
<span class="line"><span>          Hit         Miss</span></span>
<span class="line"><span>           │           │</span></span>
<span class="line"><span>           ↓           ↓</span></span>
<span class="line"><span>          Use       Check OS</span></span>
<span class="line"><span>       cached IP   DNS Cache  </span></span></code></pre></div>
<p><em><strong>Cache hit:</strong> The requested data is found in the cache.</em></p>
<p><em><strong>Cache miss:</strong> The requested data isn't in the cache, so the system has to look elsewhere.</em></p>
<p>If the browser doesn't have a cached entry, or if the cached entry has expired, it asks the operating system to continue the search.</p>
<h3 id="step-2-the-operating-system-checks-its-dns-cache">Step 2: The operating system checks its DNS cache<a class="heading-anchor" href="#step-2-the-operating-system-checks-its-dns-cache" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Like the browser, modern operating systems maintain their own DNS cache. Unlike the browser's cache, however, the operating system's cache is shared across applications. This means that if another browser or application (e.g. <code>curl</code>) recently looked up the same domain, the IP address may already be stored in the operating system's cache.</p>
<p>If a valid cached entry exists, the operating system returns the IP address to the browser, which can immediately begin establishing a connection.</p>
<p>If not, the operating system forwards the DNS query to its configured recursive DNS resolver.</p>
<h3 id="step-3-the-request-reaches-a-recursive-dns-resolver">Step 3: The request reaches a recursive DNS resolver<a class="heading-anchor" href="#step-3-the-request-reaches-a-recursive-dns-resolver" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The operating system sends the DNS query to a <strong>recursive DNS resolver</strong>. This is a remote server operated by your internet service provider (ISP), your organization, or a public DNS provider.</p>
<p>It's called <strong>recursive</strong> because it takes responsibility for finding the complete answer on your behalf. Instead of simply pointing to another DNS server, it continues querying other DNS servers until it either finds the IP address or determines that the domain doesn't exist.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>            Your Computer</span></span>
<span class="line"><span>                  │</span></span>
<span class="line"><span>                  │ DNS query</span></span>
<span class="line"><span>                  ↓</span></span>
<span class="line"><span>  ┌─────────────────────────────┐</span></span>
<span class="line"><span>  │    Recursive DNS Resolver   │</span></span>
<span class="line"><span>  └─────────────────────────────┘</span></span>
<span class="line"><span>                  │</span></span>
<span class="line"><span>       "I'll continue searching</span></span>
<span class="line"><span>        until I find the answer."</span></span>
<span class="line"><span>                  │</span></span>
<span class="line"><span>                  ↓</span></span>
<span class="line"><span>          Other DNS servers...</span></span>
<span class="line"><span>                  │</span></span>
<span class="line"><span>                  ↓</span></span>
<span class="line"><span>           Final IP address</span></span>
<span class="line"><span>                  │</span></span>
<span class="line"><span>                  ↓</span></span>
<span class="line"><span>            Your Computer</span></span></code></pre></div>
<p>Before contacting other DNS servers, the resolver first checks its own cache. If another user recently requested the same domain and the cached record is still valid, it can immediately return the IP address to your computer.</p>
<p>If the answer isn't cached, the resolver begins following the DNS hierarchy. Rather than contacting DNS servers at random, it queries them in a specific order:</p>
<ol>
<li>Ask a <strong>root nameserver</strong> which <strong>top-level domain (TLD) nameserver</strong> to contact.</li>
<li>Ask the <strong>TLD nameserver</strong> which authoritative nameserver to contact.</li>
<li>Ask the <strong>authoritative nameserver</strong> for the domain's IP address.</li>
</ol>
<p>Don't worry if some of these terms are unfamiliar. We'll explain what each server does and walk through every step in the following sections.</p>
<h3 id="step-4-the-resolver-asks-a-root-nameserver">Step 4: The resolver asks a root nameserver<a class="heading-anchor" href="#step-4-the-resolver-asks-a-root-nameserver" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>If the recursive DNS resolver can't find the answer in its cache, it begins searching the DNS hierarchy.</p>
<p>The first stop is a <strong>root nameserver</strong>. A root nameserver is a special DNS server at the very top of the DNS hierarchy. Rather than storing the IP addresses of individual websites, it acts like a directory, knowing which nameservers are responsible for each top-level domain (TLD), such as <code>.com</code>, <code>.dev</code>, <code>.org</code>, and <code>.net</code>.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>               Root Nameserver</span></span>
<span class="line"><span>            "Who manages each TLD?"</span></span>
<span class="line"><span>   ┌─────────────────┼─────────────────┐</span></span>
<span class="line"><span>   ↓                 ↓                 ↓</span></span>
<span class="line"><span>┌─────────┐     ┌─────────┐      ┌─────────┐</span></span>
<span class="line"><span>│  .com   │     │  .org   │      │  .dev   │</span></span>
<span class="line"><span>│   TLD   │     │   TLD   │      │   TLD   │</span></span>
<span class="line"><span>│ Server  │     │ Server  │      │ Server  │</span></span>
<span class="line"><span>└─────────┘     └─────────┘      └─────────┘</span></span></code></pre></div>
<p>So the resolver asks:</p>
<blockquote>
<p>"Where can I find information about <code>themissinglevel.dev</code>?"</p>
</blockquote>
<p>The root nameserver doesn't return an IP address. Instead, it replies with the addresses of the nameservers responsible for the <code>.dev</code> top-level domain.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Resolver                Root Nameserver</span></span>
<span class="line"><span>   │                           │</span></span>
<span class="line"><span>   │ Where can I find          │</span></span>
<span class="line"><span>   │ themissinglevel.dev?      │</span></span>
<span class="line"><span>   │──────────────────────────>│</span></span>
<span class="line"><span>   │                           │</span></span>
<span class="line"><span>   │ "I don't know, but ask    │</span></span>
<span class="line"><span>   │ one of the .dev           │</span></span>
<span class="line"><span>   │ nameservers."             │</span></span>
<span class="line"><span>   │&#x3C;──────────────────────────│</span></span></code></pre></div>
<h3 id="step-5-the-resolver-asks-a-tld-nameserver">Step 5: The resolver asks a TLD nameserver<a class="heading-anchor" href="#step-5-the-resolver-asks-a-tld-nameserver" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Using the information returned by the root nameserver, the resolver contacts one of the <code>.dev</code> TLD nameservers.</p>
<p>A <strong>top-level domain (TLD) nameserver</strong> knows which authoritative nameserver is responsible for every domain registered under its TLD. It doesn't know the domain's IP address, but it knows where to find the server that does.</p>
<p>The resolver asks:</p>
<blockquote>
<p>"Who is responsible for <code>themissinglevel.dev</code>?"</p>
</blockquote>
<p>The TLD nameserver replies with the address of the domain's <strong>authoritative nameserver</strong>.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Resolver              .dev TLD Nameserver</span></span>
<span class="line"><span>   │                           │</span></span>
<span class="line"><span>   │ Where can I find          │</span></span>
<span class="line"><span>   │ themissinglevel.dev?      │</span></span>
<span class="line"><span>   │──────────────────────────>│</span></span>
<span class="line"><span>   │                           │</span></span>
<span class="line"><span>   │ "Ask this authoritative   │</span></span>
<span class="line"><span>   │ nameserver."              │</span></span>
<span class="line"><span>   │ Authoritative Nameserver  │</span></span>
<span class="line"><span>   │&#x3C;──────────────────────────│</span></span></code></pre></div>
<h3 id="step-6-the-resolver-asks-the-authoritative-nameserver">Step 6: The resolver asks the authoritative nameserver<a class="heading-anchor" href="#step-6-the-resolver-asks-the-authoritative-nameserver" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Using the information returned by the TLD nameserver, the resolver contacts the <strong>authoritative nameserver</strong> for <code>themissinglevel.dev</code>.</p>
<p>An <strong>authoritative nameserver</strong> stores the official DNS records for a domain and provides the authoritative answer during a DNS lookup. Unlike the root and TLD nameservers, it has the final answer.</p>
<p>The resolver asks:</p>
<blockquote>
<p>"What is the IP address of <code>themissinglevel.dev</code>?"</p>
</blockquote>
<p>The authoritative nameserver looks up the requested DNS record and replies:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>themissinglevel.dev → 203.0.113.42</span></span></code></pre></div>
<p>The resolver now has the answer it was looking for. Before returning it to your computer, it usually stores the result in its cache for a period of time so future requests for the same domain can be answered much faster.</p>
<p>Finally, the resolver returns the IP address to your operating system, which then passes it to the browser.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>           Resolver</span></span>
<span class="line"><span>               │</span></span>
<span class="line"><span> "What's the IP address of</span></span>
<span class="line"><span>  themissinglevel.dev?"</span></span>
<span class="line"><span>               │</span></span>
<span class="line"><span>               ↓</span></span>
<span class="line"><span> ┌──────────────────────────┐</span></span>
<span class="line"><span> │ Authoritative Nameserver │</span></span>
<span class="line"><span> └──────────────────────────┘</span></span>
<span class="line"><span>               │</span></span>
<span class="line"><span>               │ 203.0.113.42</span></span>
<span class="line"><span>               ↓</span></span>
<span class="line"><span>            Resolver</span></span></code></pre></div>
<h3 id="step-7-the-browser-can-finally-connect-to-the-server">Step 7: The browser can finally connect to the server<a class="heading-anchor" href="#step-7-the-browser-can-finally-connect-to-the-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>At this point, DNS has finished its job.</p>
<p>The browser now knows the website's IP address, so it can begin establishing a connection.</p>
<p>This typically involves:</p>
<ol>
<li>Opening a TCP connection.</li>
<li>Performing a TLS handshake if the site uses HTTPS.</li>
<li>Sending the first HTTP request.</li>
</ol>
<p>That first HTTP request also includes request headers, which tell the server things like which content types the browser accepts, whether it already has cached resources, and much more. If you're curious about what those headers do and which ones you'll encounter most often, check out <a href="/seven-http-request-headers-every-developer-should-understand/">7 HTTP request headers every developer should understand</a>.</p>
<p>Once the server receives the request, it can start sending back the HTML, images, CSS, Javascript, and everything else needed to display the page.</p>
<blockquote>
<p><strong>DNS doesn't download websites. It simply tells your browser where to find them.</strong></p>
</blockquote>
<p>If you'd like to follow what happens next, from the TCP connection and TLS handshake all the way to HTML parsing and rendering the first pixel on screen, continue with <a href="/what-happens-after-you-press-enter/">What happens after you press Enter</a>.</p>
<h2 id="what-happens-if-dns-cant-find-the-domain">What happens if DNS can't find the domain?<a class="heading-anchor" href="#what-happens-if-dns-cant-find-the-domain" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Sometimes a DNS lookup fails because the requested domain or DNS record can't be found.</p>
<p>If the authoritative nameserver doesn't contain the requested domain or record, it returns a response indicating that the domain doesn't exist (commonly called <strong>NXDOMAIN</strong>, short for <strong>Non-Existent Domain</strong>).</p>
<p>When that happens, the resolver has nowhere else to look, so it returns the failure to your computer.</p>
<p>Your browser then displays an error such as:</p>
<ul>
<li>"This site can't be reached"</li>
<li>"Server not found"</li>
<li>"DNS_PROBE_FINISHED_NXDOMAIN"</li>
</ul>
<p>In many cases, the problem isn't that the website is offline. The domain simply couldn't be resolved to an IP address.</p>
<h2 id="a-real-dns-lookup-example">A real DNS lookup example<a class="heading-anchor" href="#a-real-dns-lookup-example" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>You can perform your own DNS lookup from the command line using the <code>dig</code> (<strong>Domain Information Groper</strong>) command. It's a command-line tool for querying DNS servers and is commonly available on Linux and macOS. Windows users can install it through tools such as BIND or use the built-in <code>nslookup</code> command instead.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">dig</span><span style="color:#9ECBFF"> themissinglevel.dev</span></span></code></pre></div>
<p>Among the output, you'll see an <strong>ANSWER SECTION</strong> similar to this:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>;; ANSWER SECTION:</span></span>
<span class="line"><span>themissinglevel.dev.    300    IN    A    104.21.14.102</span></span>
<span class="line"><span>themissinglevel.dev.    300    IN    A    172.67.158.160</span></span></code></pre></div>
<p>Breaking this down:</p>
<ul>
<li>
<p><strong><code>themissinglevel.dev</code></strong>: the domain name that was queried.</p>
</li>
<li>
<p><strong><code>300</code></strong>: the <strong>TTL (Time To Live)</strong>, measured in seconds. It tells DNS resolvers how long they can cache this answer before asking again. In this example, the result can be cached for <strong>5 minutes</strong>.</p>
</li>
<li>
<p><code>IN</code>: indicates that this record belongs to the Internet. You'll see <code>IN</code> in almost every DNS lookup.</p>
</li>
<li>
<p><strong><code>A</code></strong>: the DNS <strong>record type</strong>. An <strong>A record</strong> maps a domain name to an <strong>IPv4 address</strong>. (For IPv6 addresses, DNS uses an <strong>AAAA</strong> record instead.)</p>
</li>
<li>
<p><strong><code>104.21.14.102</code></strong> and <strong><code>172.67.158.160</code></strong>: the IPv4 addresses returned for the domain. Notice that there are <strong>two A records</strong>. This is common for websites that use multiple servers or a CDN, allowing traffic to be distributed across different IP addresses for better performance and reliability.</p>
</li>
</ul>
<h2 id="common-misconceptions-about-dns">Common misconceptions about DNS<a class="heading-anchor" href="#common-misconceptions-about-dns" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="dns-and-http-are-not-the-same-thing">DNS and HTTP are not the same thing<a class="heading-anchor" href="#dns-and-http-are-not-the-same-thing" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>DNS happens <strong>before</strong> HTTP.</p>
<p>DNS finds the server's IP address. HTTP is then used to request pages and other resources from that server.</p>
<h3 id="dns-doesnt-know-what-page-youre-visiting">DNS doesn't know what page you're visiting<a class="heading-anchor" href="#dns-doesnt-know-what-page-youre-visiting" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>DNS only resolves the domain name.</p>
<p>Whether you visit:</p>
<ul>
<li><code>example.com</code></li>
<li><code>example.com/about</code></li>
<li><code>example.com/blog/article</code></li>
</ul>
<p>the DNS lookup is exactly the same because the domain hasn't changed.</p>
<h3 id="dns-isnt-one-giant-database">DNS isn't one giant database<a class="heading-anchor" href="#dns-isnt-one-giant-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>DNS is a distributed system made up of thousands of servers around the world.</p>
<p>Each server is responsible for only a small part of the overall hierarchy.</p>
<h2 id="how-do-you-point-a-domain-to-your-server">How do you point a domain to your server?<a class="heading-anchor" href="#how-do-you-point-a-domain-to-your-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Imagine you've just deployed a new website to a VPS with the IP address <code>203.0.113.42</code>.</p>
<p>If you're setting up a brand-new domain, it's also worth checking whether it has been used before. A domain's previous history can affect SEO, email deliverability, and even browser trust. I explain why in <a href="/why-you-should-check-a-domains-history/">Why you should check a domain's history before buying it</a>.</p>
<p>To make <code>mywebsite.com</code> point to your server, you log in to your DNS provider (such as Cloudflare) and create an <strong>A record</strong>:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Type: A</span></span>
<span class="line"><span>Name: @</span></span>
<span class="line"><span>Value: 203.0.113.42</span></span></code></pre></div>
<p>When someone later visits <code>https://mywebsite.com</code>, their browser performs a DNS lookup, resolves the domain name to <code>203.0.113.42</code>, and then connects to your server.</p>
<p>Without that DNS record, browsers wouldn't know where your website is hosted.</p>
<blockquote>
<p><strong>Remember the DNS caches from earlier?</strong> Even if your new DNS record has propagated successfully, your browser, operating system, or DNS resolver may still have the previous result cached. If that happens, you might not see your website immediately. Waiting for the cache to expire, or clearing your local DNS cache, usually resolves the issue.</p>
</blockquote>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="can-one-domain-have-multiple-ip-addresses">Can one domain have multiple IP addresses?<a class="heading-anchor" href="#can-one-domain-have-multiple-ip-addresses" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Yes.</p>
<p>A domain can have multiple <strong>A records</strong> (which map a domain to IPv4 addresses) or multiple <strong>AAAA records</strong> (which map a domain to IPv6 addresses).</p>
<p>In many cases, each IP address belongs to a different server. When someone visits the domain, DNS can return multiple IP addresses, allowing requests to be distributed across those servers. This helps with load balancing, redundancy, and improving availability.</p>
<p>For example:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>example.com.    A    192.0.2.10</span></span>
<span class="line"><span>example.com.    A    192.0.2.11</span></span></code></pre></div>
<p>When a DNS resolver looks up <code>example.com</code>, it may receive both IP addresses and connect to one of them.</p>
<h3 id="is-dns-secure-during-a-dns-lookup">Is DNS secure during a DNS lookup?<a class="heading-anchor" href="#is-dns-secure-during-a-dns-lookup" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>By default, DNS queries are sent without encryption, which means other systems on the network may be able to observe which domains you're looking up.</p>
<p>Technologies such as <strong>DNS over HTTPS (DoH)</strong> and <strong>DNS over TLS (DoT)</strong> encrypt DNS queries while they're being transmitted, making them much harder to intercept.</p>
<p>However, encryption only protects the communication between your device and the DNS resolver. It doesn't guarantee that the DNS response itself is genuine.</p>
<p>That's where <strong>DNSSEC (Domain Name System Security Extensions)</strong> comes in. DNSSEC adds digital signatures to DNS records, allowing resolvers to verify that the response really came from the domain's authoritative nameserver and wasn't altered or forged along the way.</p>
<p>Without DNSSEC, an attacker could potentially forge a DNS response and redirect users to the wrong server.</p>
<h2 id="dns-troubleshooting-tips-worth-remembering">DNS troubleshooting tips worth remembering<a class="heading-anchor" href="#dns-troubleshooting-tips-worth-remembering" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Here are a few practical things worth remembering the next time you're working with domains or debugging network issues:</p>
<ul>
<li><strong>Not every connection problem is a server problem.</strong> If a domain can't be resolved, the browser never reaches your server.</li>
<li><strong>DNS changes aren't always visible immediately.</strong> Before assuming propagation is still in progress, remember that your browser, operating system, or DNS resolver may have cached the previous result. You may need to wait for the cache to expire or clear your local DNS cache.</li>
<li><strong>One domain can point to multiple IP addresses.</strong> This is commonly used for load balancing, redundancy, and CDNs.</li>
<li><strong>Keep <code>dig</code> in your toolbox.</strong> It's one of the quickest ways to verify DNS records and troubleshoot domain-related issues.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>7 HTTP request headers every developer should understand</title>
      <link>https://themissinglevel.dev/seven-http-request-headers-every-developer-should-understand/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/seven-http-request-headers-every-developer-should-understand/</guid>
      <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
      <description>Every HTTP request carries headers describing who is asking and what they want. Learn the 7 request headers you will meet most often, with examples.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>There is a good chance you've seen HTTP headers hundreds of times without paying much attention to them.</p>
<p>I know I have.</p>
<p>In my day to day work, I deal with HTTP requests and responses all the time. I inspect response objects, check status codes and look at response bodies when something goes wrong or when I'm building something new.</p>
<p>Yet somehow, I never spent much time looking at the headers themselves. They were always there, but they felt like background noise rather than something worth understanding.</p>
<p>So what changed? Why did I decide to look at them now?</p>
<p>I'm not sure there was a specific reason. It just occurred to me that I'd spent years walking past this part of HTTP without ever stopping to see what was there. It's a bit like living in the same city for years and suddenly deciding to visit a museum you've walked past hundreds of times. It was always there. I just never made the time.</p>
<p>Then one day, it simply felt like it was time.</p>
<p>If HTTP requests and responses are such a fundamental part of web development, why had I never stopped to understand this piece of the conversation?</p>
<p>One thing I've noticed about myself is that I understand new concepts much more easily when I can actually see them. I don't know whether it's the frontend side of me talking, but I've always found it easier to learn when I have something tangible to look at: a log, a network request, a metric or some real output. It helps me visualize what's happening.</p>
<p>HTTP headers turned out to be perfect for that because you don't have to imagine anything about them. You can inspect HTTP headers yourself.</p>
<p>Open your browser's <strong>Developer Tools</strong>, switch to the <strong>Network</strong> tab and click on any request, or use a CLI tool like <code>curl</code>.</p>
<p>In this article, we'll use <code>curl</code> because it gives us a clear view of the HTTP conversation.</p>
<p>Enough talking, let's look at a real request.</p>
<p>We'll make a simple <code>GET</code> request to <code>example.com</code>, a domain reserved specifically for documentation and examples.</p>
<p>If you're curious about everything that happens before the browser sends this request, I've written about the <a href="/what-happens-after-you-press-enter/">journey from typing a URL to sending an HTTP request</a>.</p>
<p>All we need is:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -v</span><span style="color:#9ECBFF"> https://example.com</span></span></code></pre></div>
<p>The <code>-v</code> (verbose) flag tells <code>curl</code> to show the full HTTP conversation, including both the request headers sent by the client and the response headers returned by the server.</p>
<p>The output contains much more than just the headers. It also includes information about the network connection and the TLS handshake. To keep things simple, I've included only the header portion below.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">> GET / HTTP/2</span></span>
<span class="line"><span style="color:#E1E4E8">> Host: example.com</span></span>
<span class="line"><span style="color:#E1E4E8">> User-Agent: curl/8.7.1</span></span>
<span class="line"><span style="color:#E1E4E8">> Accept: */*</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">&#x3C; HTTP/2 200</span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C; Content-Type: text/html</span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C; Content-Length: 1248</span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C; Cache-Control: max-age=3600</span></span></code></pre></div>
<p>The <code>></code> lines are the request headers sent by the client, while the <code>&#x3C;</code> lines are the response headers returned by the server.</p>
<p>Even if you've never looked at request and response headers before, one thing probably stands out immediately.</p>
<p>Most of the names are plain English.</p>
<p><code>Host</code>, <code>User-Agent</code>, <code>Content-Type</code> and <code>Cache-Control</code> are all fairly descriptive once you stop and read them. You don't need to memorize every HTTP header. After you understand a handful of the common ones, you'll start recognizing the patterns.</p>
<p>Every HTTP header follows the same structure. It's simply a <strong>name</strong> followed by a <strong>value</strong>, separated by a colon.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Host</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> example.com</span></span>
<span class="line"><span style="color:#85E89D">Content-Type</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> text/html</span></span></code></pre></div>
<p>The name tells you what information is being sent. The value contains that information.</p>
<p>As developers, we've all worked with key-value pairs in one form or another, whether it's Javascript objects, dictionaries or maps. HTTP headers follow exactly the same idea. They're simply a collection of key-value pairs describing the request or the response.</p>
<p>Now that we've seen what HTTP headers look like, let's take a step back. Before we look at what each header does, it's worth asking a more fundamental question:</p>
<h2 id="why-do-http-headers-exist">Why do HTTP headers exist?<a class="heading-anchor" href="#why-do-http-headers-exist" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>I started wondering about headers by trying to remove them from the equation to better understand their role.</p>
<blockquote>
<p><strong>Do we actually need HTTP headers?</strong></p>
</blockquote>
<p>After all, the browser wants a page and the server sends one back. Why isn't the conversation simply a request followed by a response?</p>
<p>Let's ignore headers for a moment.</p>
<p>Imagine your browser sends a request, the server replies with a stream of bytes and that's the end of the conversation.</p>
<p>The browser immediately has a problem.</p>
<p>What are those bytes? Are they HTML, JSON or an image? Should they be displayed in the browser or downloaded as a file? Can they be cached? Did the server create a <a href="/session-vs-session-cookie-vs-sessionstorage/">session cookie</a>?</p>
<p>My first thought was that perhaps the browser could simply inspect the data and figure it out. For some file formats, that's actually possible. Images, PDFs and other file types often have distinctive signatures that make them easier to identify.</p>
<p>The more I thought about it, the less I liked the idea of relying on guesses. Not every format is easy to identify, different browsers could make different decisions and, as we'll see in a future article, guessing content has caused real security problems in the past. Then how was this solved?</p>
<p><strong>HTTP takes a much simpler approach.</strong></p>
<p>Instead of asking the browser to guess, the sender provides information about what it's sending before the actual data arrives.</p>
<p>That kind of information has a name: <strong>metadata</strong>.</p>
<p>You've probably heard the phrase <em>"data about data"</em> before. That's exactly what metadata is. It isn't the content itself; it's information that describes the content.</p>
<p>And that's exactly what HTTP request and response headers are.</p>
<p>They don't contain the page itself. They contain metadata about the request or the response, helping the other side understand what it's about to receive and how it should handle it.</p>
<p>Think of headers as instructions that travel alongside the request or response rather than the content itself.</p>
<h2 id="what-is-the-difference-between-request-and-response-headers">What is the difference between request and response headers?<a class="heading-anchor" href="#what-is-the-difference-between-request-and-response-headers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>One thing that surprised me while learning about HTTP headers was realizing that they aren't just something the server sends back.</p>
<p>Every HTTP conversation contains two sets of headers. The client sends request headers before the request, and the server sends response headers before the response. Together, they describe the two halves of the HTTP conversation.</p>
<p>This article focuses on request headers. In the next one, we'll switch sides and explore the response headers sent back by the server. Stay tuned!</p>
<h2 id="the-7-most-common-http-request-headers">The 7 most common HTTP request headers<a class="heading-anchor" href="#the-7-most-common-http-request-headers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Every HTTP request includes a collection of HTTP request headers. Some identify the client, others describe what it expects from the server, and some carry information that allows the server to recognize or authorize the request.</p>
<p>The first few headers tell the server who is making the request and which website the request is intended for.</p>
<h3 id="host-header">Host header<a class="heading-anchor" href="#host-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The <code>Host</code> header tells the server which website the browser is trying to access. You'll typically see it like this:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Host</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> example.com</span></span></code></pre></div>
<p>If it feels confusing don't worry. At first this confused me too.</p>
<p>Didn't the browser already connect to <code>example.com</code>? Why does it need to send the domain name again?</p>
<p>Here's what actually happens:</p>
<ul>
<li>When you type a URL such as <code>example.com</code>, one of the first things that happens is that DNS resolves the domain name to an IP address.</li>
<li>The browser then sends the request to that IP address.</li>
<li>When the request reaches the server, the server already knows which IP address the request arrived on. What it doesn't know yet is <strong>which website</strong> hosted on that server should handle it.</li>
<li>That's where the <code>Host</code> header comes in. It tells the server exactly which website the client is trying to access.</li>
</ul>
<p>This becomes much easier to understand when multiple websites share the same <a href="/what-is-a-server-really/">server</a>. Imagine two different domains both point to the same IP address:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>example.com -> 203.0.113.10</span></span>
<span class="line"><span>example.net -> 203.0.113.10</span></span></code></pre></div>
<p>Both requests arrive at exactly the same server. Without the <code>Host</code> header, the server wouldn't know whether you wanted <code>example.com</code> or <code>example.net</code>.</p>
<p>If you ever visit the IP address directly instead of the domain, you'll often see a completely different website or the server's default page. That's because the server uses the <code>Host</code> header to decide which website should handle the request.</p>
<h3 id="user-agent-header">User-Agent header<a class="heading-anchor" href="#user-agent-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>This one is a little easier to understand.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">User-Agent</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/138.0</span></span></code></pre></div>
<p>The <code>User-Agent</code> header answers a simple question:</p>
<blockquote>
<p><strong>What software is making this request?</strong></p>
</blockquote>
<p>Notice that it doesn't identify <strong>you</strong>. It doesn't contain your username or prove your identity. Instead, it describes the client sending the request. For example, it could tell the server that the request came from:</p>
<ul>
<li>Google Chrome</li>
<li>Mozilla Firefox</li>
<li>a search engine crawler such as Googlebot</li>
<li>a CLI tool like curl</li>
</ul>
<p>So why is this useful?</p>
<p>Historically, websites often returned slightly different content depending on the browser because browsers supported different features. Today, a much more common use is analytics, allowing website owners to see which browsers and devices are visiting their site. Search engine crawlers also identify themselves using the <code>User-Agent</code> header, making it easy for servers to recognize that the request is coming from a crawler.</p>
<p>One important thing to remember is that the <code>User-Agent</code> header should never be trusted for authentication or authorization. It's just information provided by the client, and the client can change it to anything. That applies to every header on this page, and it's a specific case of the broader rule that <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a>.</p>
<p>For example, this command makes <code>curl</code> identify itself as Googlebot:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -v</span><span style="color:#79B8FF"> -A</span><span style="color:#9ECBFF"> "Googlebot"</span><span style="color:#9ECBFF"> https://example.com</span></span></code></pre></div>
<p>The server receives:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">User-Agent</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> Googlebot</span></span></code></pre></div>
<p>even though the request was sent by <code>curl</code>.</p>
<p>In other words, the <code>User-Agent</code> is useful as a hint, but it should never be treated as proof of identity.</p>
<blockquote>
<p><strong>User-Agent ≠ Authentication</strong></p>
</blockquote>
<p>After introducing itself, the browser also explains what it's expecting in return. These headers describe the formats, languages and compression methods the client is willing to accept.</p>
<h3 id="accept-header">Accept header<a class="heading-anchor" href="#accept-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The <code>Accept</code> header tells the server what kind of response the client is willing to receive.</p>
<p>For example:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Accept</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> text/html</span></span></code></pre></div>
<p>In this case, the client is saying that it's expecting an HTML document. Other common values include:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Accept</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> application/json</span></span>
<span class="line"><span style="color:#85E89D">Accept</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> image/webp</span></span>
<span class="line"><span style="color:#85E89D">Accept</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> */*</span></span></code></pre></div>
<p>The client is expressing a preference, not giving the server an order. The server decides whether it can satisfy that preference. If it can't, it may respond with <strong>406 Not Acceptable</strong>, although in practice many servers simply ignore the header and return their default format.</p>
<p>You'll also come across other <code>Accept-*</code> headers that follow the same idea.</p>
<p>For example, <code>Accept-Language</code> tells the server which languages the client prefers:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Accept-Language</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> fr,en;q=0.8</span></span></code></pre></div>
<p>The <strong>q</strong> value indicates preference. In this example, the client prefers French but is also willing to accept English if French isn't available.</p>
<h3 id="accept-encoding-header">Accept-Encoding header<a class="heading-anchor" href="#accept-encoding-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Another common one is <code>Accept-Encoding</code>, which tells the server which compression formats the client understands.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Accept-Encoding</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> gzip, br</span></span></code></pre></div>
<p>In other words, the browser is saying:</p>
<blockquote>
<p><em>"If you want to compress the response before sending it, I know how to decompress gzip and Brotli (br)."</em></p>
</blockquote>
<p>If the server decides to compress the response, it lets the browser know by including a <code>Content-Encoding</code> response header. The browser then automatically decompresses the response before displaying the page. The entire process is completely invisible to the user.</p>
<p>💡 <strong>Did you know?</strong><br>
Many HTML, CSS and Javascript files become <strong>70–90% smaller</strong> when compressed with modern algorithms like Gzip or Brotli. That's one of the reasons websites can load much faster, even though the browser still receives exactly the same content after decompressing it.</p>
<h3 id="origin-header">Origin header<a class="heading-anchor" href="#origin-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>One more request header worth knowing is <code>Origin</code>.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#85E89D">Origin</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> https://example.com</span></span></code></pre></div>
<p>Unlike the headers we've looked at so far, the <code>Origin</code> header tells the server <strong>which website initiated the request</strong>. You'll most often encounter it when building or consuming APIs that are called directly from a browser.</p>
<p>For example, imagine your frontend is running at <code>https://app.example.com</code> and it sends a request to an API at <code>https://api.example.com</code>. The browser automatically includes the <code>Origin</code> header, allowing the server to decide whether the cross-origin request should be allowed.</p>
<p>So far, we've looked at request headers that identify the client and describe the kind of response it can handle. Some request headers serve a different purpose: they help the server recognize the client or verify that it has permission to access a protected resource.</p>
<p>Unlike the <code>Referer</code> header, which can include the full URL of the previous page, the <code>Origin</code> header contains only the origin (scheme, host and port). That's one of the reasons browsers use it when enforcing CORS policies.</p>
<h3 id="cookie-header">Cookie header<a class="heading-anchor" href="#cookie-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The <code>Cookie</code> header is how the browser sends information it has previously stored back to the server. Most commonly, this includes a session identifier that allows the server to recognize a returning user.</p>
<p>Imagine you log in to a website. The server doesn't remember you simply because you're using the same browser. Instead, it <a href="/why-your-cookie-is-not-being-set/">asks the browser to store</a> a small piece of information called a cookie. On every future request, the browser automatically sends that cookie back using the Cookie header. This is one of the most common ways websites <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">keep you logged in</a> as you move from page to page.</p>
<h3 id="authorization-header">Authorization header<a class="heading-anchor" href="#authorization-header" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Unlike <code>Cookie</code>, the <code>Authorization</code> header is used when the client wants to explicitly prove it has permission to access a protected resource. Instead of relying on information stored by the browser, the client includes credentials such as an API key or an access token directly in the request.</p>
<p>You'll encounter this header frequently when working with APIs. If you're wondering how a website can verify your identity without ever storing your password, I wrote about <a href="/why-websites-cant-tell-you-your-password/">how password hashing works and why websites can't tell you your password</a>.</p>
<p>For example, after logging into an application, the client might receive an access token and include it in every subsequent request. The server validates that token before deciding whether to return the requested resource or reject the request.</p>
<p>Although both headers are commonly involved in <a href="/how-user-authentication-works/">authentication</a>, they serve different purposes. Cookies are usually managed automatically by the browser, while the <code>Authorization</code> header is typically added explicitly by the client or application.</p>
<h2 id="what-to-remember-about-http-request-headers">What to remember about HTTP request headers<a class="heading-anchor" href="#what-to-remember-about-http-request-headers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<ul>
<li>Every HTTP request includes one or more HTTP request headers. Some identify the client, others describe the response it can handle, and some carry authentication or session information.</li>
<li>Request headers are metadata sent before the request body.</li>
<li>The <code>Host</code> header tells the server which website should handle the request.</li>
<li>The <code>User-Agent</code> header identifies the software making the request, not the user.</li>
<li>The <code>Accept</code> family of headers tells the server what kinds of responses the client can handle.</li>
<li><code>Cookie</code> and <code>Authorization</code> are commonly used to recognize or authenticate a client, but they serve different purposes.</li>
<li>Understanding request headers makes browser DevTools and network debugging much easier.</li>
</ul>
<p>The next time you open your browser's Network tab, don't skip over the headers. They're no longer just a list of key-value pairs, they're the browser and the server explaining exactly what they're about to do.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why you should check a domain's history before buying it</title>
      <link>https://themissinglevel.dev/why-you-should-check-a-domains-history/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-you-should-check-a-domains-history/</guid>
      <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
      <description>Domains are secondhand goods. Search engines, browsers and mail servers all keep long memories about names, and you inherit them at checkout.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>You've found it.</p>
<p>The perfect domain for your next project is somehow still available.
It's short, memorable and surprisingly inexpensive. Your cursor is
already hovering over the <strong>Buy</strong> button when a simple question pops
into your head.</p>
<blockquote>
<p><em>Has anyone owned this domain before?</em></p>
</blockquote>
<p>Most of us don't think about that. If the registrar says the domain is
available, we assume it's a fresh start. That's what I used to think
too.</p>
<p>Then, while searching for a domain for a personal project, I became
curious. I wanted to know whether someone else had used it before me. I
expected to find almost nothing. Instead, I discovered that domain names
can carry years of history, and sometimes that history matters.</p>
<p>A domain isn't just a name. In many ways, it's an <strong>identity</strong> that has
existed on the internet long before you arrived.</p>
<h2 id="a-domain-has-a-memory">A domain has a memory<a class="heading-anchor" href="#a-domain-has-a-memory" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Search engines, browsers and email providers don't really know who owns
a domain. They mostly focus the domain itself.</p>
<p>Imagine that someone spent years running a legitimate blog on a domain.
Over time, other websites linked to it, people bookmarked its pages and
search engines learned to trust it.</p>
<p>Now imagine the opposite.</p>
<p>Perhaps the domain was used for spam, fake online shops or phishing
pages. Maybe thousands of low-quality websites linked to it in an
attempt to manipulate search rankings. Even after the owner disappears,
some of that history can remain.</p>
<p>That doesn't mean every old domain is a bad purchase. In fact, most
aren't.</p>
<p>It simply means that buying a domain is sometimes more like buying a
second-hand car than buying a brand-new one.You don't just inherit the
keys. You may also inherit part of its story.</p>
<h2 id="what-kind-of-history-can-you-inherit">What kind of history can you inherit?<a class="heading-anchor" href="#what-kind-of-history-can-you-inherit" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The most common thing is an old backlink profile.</p>
<p>If you're not familiar with the term, a <strong>backlink</strong> is simply a link from another website to yours. Search engines use backlinks as one of many signals to understand how websites are connected and how trustworthy they might be.</p>
<p>Years ago, many website owners tried to improve their rankings by
creating thousands of artificial backlinks. Google has become much
better at ignoring those today, but they don't disappear overnight. If a
domain has a strange backlink profile, it's worth knowing before you
build your next project on it.</p>
<p>Another possibility is old content.</p>
<p>Even after a website disappears, search engines and other websites may
still remember it. Visitors might continue trying to access pages that
no longer exist. You'll sometimes notice strange URLs appearing in your
analytics, simply because someone bookmarked them years ago.</p>
<p>There are also more serious cases.</p>
<p>Domains that previously hosted malware or phishing pages may still
appear in security databases. Domains used for sending spam can have a
damaged email reputation.</p>
<p>None of these problems are impossible to fix, but they're much easier to
avoid than repair.</p>
<h2 id="how-i-investigate-a-domain">How I investigate a domain<a class="heading-anchor" href="#how-i-investigate-a-domain" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Whenever I find a domain that looks promising, I spend a few minutes checking its past. I'm not interested in who owns it today. I want to know whether someone owned it before me, what they used it for and whether they left behind anything that could become my problem later.</p>
<p>The whole process usually takes less than ten minutes, and it may save you from buying a domain you'll later regret.</p>
<h3 id="1-check-the-wayback-machine">1. Check the Wayback Machine<a class="heading-anchor" href="#1-check-the-wayback-machine" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>The first stop is always the Wayback Machine. Instead of showing what a website looks like today, it lets you browse snapshots from years ago. Sometimes I find a legitimate business that simply shut down. Sometimes it's an abandoned personal blog. Occasionally, I discover something I'd rather avoid entirely.</p>
<p>This single check often tells me whether the domain has a normal history or whether something feels off.</p>
<h3 id="2-check-the-registration-status">2. Check the registration status<a class="heading-anchor" href="#2-check-the-registration-status" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Next, I check the domain with RDAP (Registration Data Access Protocol). It's the modern replacement for WHOIS and provides registration information about a domain, such as when it was first registered and whether it's currently registered.</p>
<p>You can query RDAP directly from your terminal:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -s</span><span style="color:#9ECBFF"> https://rdap.verisign.com/com/v1/domain/example.com</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> jq</span><span style="color:#9ECBFF"> '.events'</span></span></code></pre></div>
<p><em>If you're not familiar with jq, it's a command-line tool for reading and filtering JSON output.</em></p>
<p>The response contains several pieces of information about the domain. One of the most useful fields is the events:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">[</span></span>
<span class="line"><span style="color:#E1E4E8">  {</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventAction"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"registration"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventDate"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"1995-08-14T04:00:00Z"</span></span>
<span class="line"><span style="color:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#E1E4E8">  {</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventAction"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"expiration"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventDate"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"2026-08-13T04:00:00Z"</span></span>
<span class="line"><span style="color:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#E1E4E8">  {</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventAction"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"last changed"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventDate"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"2026-01-16T18:26:50Z"</span></span>
<span class="line"><span style="color:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#E1E4E8">  {</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventAction"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"last update of RDAP database"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#9ECBFF">    "eventDate"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"2026-07-14T10:24:27Z"</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">]</span></span>
<span class="line"></span></code></pre></div>
<p>The <strong>registration</strong> event tells me how long the domain has existed. If the registration date is many years old, I know the domain has a history worth investigating. If it's very recent, it may have been registered for the first time only recently.</p>
<p>If the command returns an empty response, don't assume something went wrong straight away.</p>
<p>The first time it happened to me, I thought the request had failed. It hadn't.</p>
<p>For the RDAP service I was using, an empty response meant the domain had never been registered before. No previous owners, no abandoned projects and no hidden surprises.</p>
<p>That was exactly what I was hoping to see!</p>
<h3 id="3-see-what-google-remembers">3. See what Google remembers<a class="heading-anchor" href="#3-see-what-google-remembers" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>After that, I ask Google a simple question.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>site:example.com</span></span></code></pre></div>
<p>If Google still has hundreds of pages indexed, I immediately learn what kind of website used to exist there. If nothing appears, that's usually a good sign because Google no longer has any pages associated with the domain.</p>
<p>If only one or two pages appear, I don't jump to conclusions. It could simply mean the website was very small, that Google has already removed most of its pages from the index, or that only a handful of pages were ever considered worth indexing. Either way, it's another useful clue when building the bigger picture.</p>
<p>This doesn't guarantee the domain has a clean history, but it helps me understand whether Google still associates it with its previous life.</p>
<h3 id="4-look-at-the-backlinks">4. Look at the backlinks<a class="heading-anchor" href="#4-look-at-the-backlinks" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Then I spend a minute looking at the backlink profile with a tool like Ahrefs. I'm not performing a full SEO audit or counting every referring domain. I'm simply checking whether the links look natural or whether the domain has thousands of suspicious backlinks pointing to it.</p>
<p>If something looks unusual, I'll investigate further. If everything looks normal, I move on.</p>
<h2 id="a-worked-example-checking-a-domain-before-buying-it">A worked example: checking a domain before buying it<a class="heading-anchor" href="#a-worked-example-checking-a-domain-before-buying-it" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Let's imagine you've found <code>themissinglevel.dev</code>.</p>
<p>The name is available, it's memorable and the price looks good. Before clicking <strong>Buy</strong>, you decide to spend a few minutes checking its history.</p>
<p>The first stop is the Wayback Machine. You discover that the domain previously belonged to a small indie game development blog. The snapshots are consistent over several years, the content looks legitimate and nothing immediately raises any concerns.</p>
<p>Next, you check the registration details with RDAP. The registration event shows that the domain was first registered in 2019, so you know it has some history behind it rather than being a brand-new registration.</p>
<p>After that, you ask Google a simple question.</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>site:themissinglevel.dev</span></span></code></pre></div>
<p>No pages appear in the results. That's usually a good sign because Google no longer has any content associated with the domain.</p>
<p>Finally, you take a quick look at the backlink profile using Ahrefs. There are a few dozen backlinks from game development blogs, GitHub repositories and developer forums. Everything looks natural rather than manipulated.</p>
<p>At this point, you still don't know everything about the domain, but you've removed most of the uncertainty. In less than ten minutes, you've built enough confidence to feel comfortable moving forward with the purchase.</p>
<p>Now imagine the opposite.</p>
<p>The Wayback Machine shows that the domain previously hosted online casinos. Google still has dozens of gambling-related pages indexed, and the backlink profile is filled with thousands of links from unrelated, low-quality websites.</p>
<p>Could you still buy the domain?</p>
<p>Of course.</p>
<p>Would I?</p>
<p>Probably not.</p>
<p>None of those signals automatically mean the domain is unusable. However, they don't align with the clean starting point I'm looking for. If I can choose between a domain with years of questionable history and one with little or no baggage, I'll choose the cleaner option every time.</p>
<h2 id="history-isnt-always-bad">History isn't always bad<a class="heading-anchor" href="#history-isnt-always-bad" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>It's easy to think every old domain is something to avoid.</p>
<p>That's not true.</p>
<p>Some expired domains belonged to respected businesses, universities or
popular blogs. They earned genuine trust over many years, which is
exactly why some of them sell for hundreds or even thousands of dollars.</p>
<p>The lesson isn't to avoid old domains.</p>
<p>The lesson is to understand them before you spend your money.</p>
<h2 id="key-takeaways">Key takeaways<a class="heading-anchor" href="#key-takeaways" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>If you only remember three things from this article, let them be these:</p>
<ul>
<li>An available domain isn't necessarily a brand-new domain. A few minutes of research can save you from inheriting somebody else's problems.</li>
<li>Start with the <strong>Wayback Machine</strong>. It's the quickest way to understand what a domain was used for in the past.</li>
<li>Use <strong>RDAP</strong> and a quick Google <code>site:&#x3C;your next domain></code> search to see how long the domain has existed and what Google still remembers about it.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Why websites can't tell you your password</title>
      <link>https://themissinglevel.dev/why-websites-cant-tell-you-your-password/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/why-websites-cant-tell-you-your-password/</guid>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
      <description>A well designed website does not know your password, yet it can still check that you typed it right. Learn how password hashing makes that possible.</description>
      <category>Security</category>
      <content:encoded><![CDATA[<p>I still remember the first time someone told me that a properly designed website doesn't actually know my password.</p>
<p>My first reaction was disbelief. If the server doesn't know my password, then how does it know whether I typed the correct one when I log in tomorrow? It felt like one of those statements developers repeat because it sounds clever, but the more I thought about it, the less sense it made.</p>
<p>As it turns out, it wasn't a trick at all. The explanation is surprisingly elegant, and once you understand it, you begin to appreciate how much thought has gone into protecting something as simple as a login form.</p>
<p>The interesting part is that this isn't some cutting-edge security feature used only by banks or large technology companies. It's a principle that every modern web application should follow, whether it's a small personal project or a platform serving millions of users.</p>
<h2 id="the-obvious-solution-would-actually-be-the-worst-one">The obvious solution would actually be the worst one<a class="heading-anchor" href="#the-obvious-solution-would-actually-be-the-worst-one" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Imagine you're building your first website.</p>
<p>Users need accounts, so you create a <code>users</code> table <a href="/what-is-a-database/">in your database</a> with an email address and a password column. Whenever someone signs up, you simply store whatever they typed. The next time they log in, you compare the password they entered with the one stored in the database.</p>
<p>From a programming perspective, it sounds perfectly reasonable. From a security perspective, it's a disaster waiting to happen.</p>
<table>
<thead>
<tr>
<th scope="col">Feature</th>
<th align="center" scope="col">Plain text password</th>
<th align="center" scope="col">Password hash</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Original password is stored</th>
<td align="center">✅</td>
<td align="center">❌</td>
</tr>
<tr>
<th scope="row">Human-readable</th>
<td align="center">✅</td>
<td align="center">❌</td>
</tr>
<tr>
<th scope="row">Original password can be recovered</th>
<td align="center">✅</td>
<td align="center">❌</td>
</tr>
<tr>
<th scope="row">Safe to store in a database</th>
<td align="center">❌</td>
<td align="center">✅</td>
</tr>
<tr>
<th scope="row">Can be used to verify a login</th>
<td align="center">✅</td>
<td align="center">✅</td>
</tr>
<tr>
<th scope="row">Used by modern web applications</th>
<td align="center">❌</td>
<td align="center">✅</td>
</tr>
</tbody>
</table>
<p>If someone gains access to your database, they immediately gain access to everyone's passwords. Even worse, many people reuse the same password across multiple websites. A single breach could allow attackers to access email accounts, social media profiles, online stores, and countless other services.</p>
<p>That's why storing plain text passwords is considered one of the biggest mistakes a developer can make.</p>
<h2 id="what-does-the-server-store-instead">What does the server store instead?<a class="heading-anchor" href="#what-does-the-server-store-instead" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Instead of storing the password itself, the server stores the result of a <strong>hash function</strong>.</p>
<p>You can think of a password hash as a digital fingerprint. Every time you provide the same input, you get exactly the same output. Unlike encryption, hashing is a one-way operation. It's designed to be easy to compute, but practically impossible to reverse.
For example, imagine a user chooses the password:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>myPassword123</span></span></code></pre></div>
<p>After being processed by a hashing algorithm, it could become something like:</p>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>a336f671080fb420c27461fcf4f2c8d1d87b8a5f6d6f8b5f8f13c2b7d5ad7168</span></span></code></pre></div>
<p>That's what gets stored in the database. Even changing a single character in the password produces a completely different hash.</p>
<blockquote>
<p><strong>How a password becomes a password hash</strong></p>
</blockquote>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>User creates a password</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Server receives the password</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Password is passed to the hashing algorithm</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>A password hash is generated</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Only the password hash is stored in the database</span></span></code></pre></div>
<p>If you look at that string, there's nothing that hints at the original password. It's just a long sequence of characters that appears completely random.</p>
<p>Modern applications typically use password hashing algorithms such as <strong>bcrypt</strong> or <strong>Argon2</strong>, which are specifically designed to make password cracking as difficult as possible. The important idea, though, is much simpler than the algorithms themselves. The server stores the transformed value, not the password you originally typed. It's also worth noting that the hashing happens on the server rather than in the browser, for the same reason <a href="/why-a-server-can-never-trust-your-browser/">a server can never trust your browser</a> to have done the work honestly.</p>
<h2 id="but-how-can-the-website-verify-my-password">But how can the website verify my password?<a class="heading-anchor" href="#but-how-can-the-website-verify-my-password" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>This was the part that confused me the most.</p>
<p>If the original password is gone, how can the server possibly know whether I typed it correctly the next day?</p>
<p>The trick is that it never tries to recover the original password.</p>
<p>Instead, when you log in, the server takes the password you just entered and runs it through the exact same hashing algorithm. If the newly generated password hash matches the one already stored in the database, then the passwords must have been identical.</p>
<blockquote>
<p><strong>How the server verifies your password</strong></p>
</blockquote>
<div class="code-block"><button type="button" data-copy-code aria-label="Copy code" aria-live="polite">Copy</button><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>User enters their password</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Server receives the password</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Password is passed to the hashing algorithm</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>A new password hash is generated</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Compare with the stored password hash</span></span>
<span class="line"><span>        ↓</span></span>
<span class="line"><span>Match?</span></span>
<span class="line"><span>├── Yes → User is authenticated</span></span>
<span class="line"><span>└── No  → Access denied</span></span></code></pre></div>
<p>The server never compares passwords directly. Instead, it hashes the password you just entered and compares the resulting hash with the one stored in the database.</p>
<p>That's a subtle difference, but it's one of the reasons <a href="/how-user-authentication-works/">modern authentication</a> is so secure.</p>
<h2 id="why-can-i-reset-my-password">Why can I reset my password?<a class="heading-anchor" href="#why-can-i-reset-my-password" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>At this point another question naturally appears.</p>
<p>If websites don't know my password anymore, why can I click <strong>Forgot Password</strong> and get back into my account?</p>
<p>Many people assume the website simply emails them the password it has stored.</p>
<p>A properly designed website can't do that, because it doesn't have the password anymore.</p>
<p>Instead, it generates a temporary reset token that's valid for a limited amount of time. That token proves you requested a password reset. After you choose a new password, the old hash is discarded, a new hash is generated, and the temporary token becomes useless.</p>
<p>This is also why you should immediately become suspicious if a website ever emails you your existing password.</p>
<p>If it can send your password back to you, it probably stored it in a readable form. That's a huge warning sign.</p>
<h2 id="what-happens-if-someone-steals-the-database">What happens if someone steals the database?<a class="heading-anchor" href="#what-happens-if-someone-steals-the-database" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>You might wonder whether hashing makes data breaches harmless.</p>
<p>Not quite.</p>
<p>If attackers steal a database containing password hashes, they can't immediately read everyone's passwords, but they can still try to guess them. They do this by hashing millions of common passwords and comparing the results with the stolen hashes.</p>
<p>This is exactly why modern password hashing algorithms are intentionally slow. While a normal user only logs in occasionally, an attacker might need to test billions of password guesses. Making every guess take significantly longer turns a practical attack into one that can become prohibitively expensive.</p>
<p>Hashing doesn't eliminate risk.</p>
<p>It dramatically raises the cost of attacking the system.</p>
<h2 id="one-small-design-decision-with-enormous-consequences">One small design decision with enormous consequences<a class="heading-anchor" href="#one-small-design-decision-with-enormous-consequences" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>One of the things I enjoy most about software engineering is discovering solutions that feel almost backwards.</p>
<p>Our instinct is often to collect more information because we think it will make the system more capable. In reality, good engineering is often about deciding what information you should never keep in the first place.</p>
<p>A website doesn't need to remember your password forever. It only needs a reliable way to verify that the password you enter tomorrow is the same one you chose today. Verifying the password is only the first step, though. How the website then <a href="/cookies-vs-sessions-vs-tokens-how-websites-keep-you-logged-in/">remembers you across every later request</a> is a separate problem with its own trade-offs.</p>
<p>That single design decision protects millions of users every day without them ever noticing.</p>
<p>The next time you sign into a website, it's worth remembering that your password probably isn't sitting in a database somewhere waiting to be read. In a well-designed system, it disappeared the moment you created your account, leaving behind only a mathematical fingerprint that proves you know the secret without revealing the secret itself.</p>
<p>In my opinion, that's one of those elegant ideas that makes software engineering so fascinating. Once you understand it, you'll never look at a login form in quite the same way again.</p>
<h2 id="frequently-asked-questions">Frequently asked questions<a class="heading-anchor" href="#frequently-asked-questions" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<h3 id="can-a-website-see-my-password">Can a website see my password?<a class="heading-anchor" href="#can-a-website-see-my-password" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A properly designed website never stores your original password. Instead, it stores a password hash. When you log in, it hashes the password you entered and compares the resulting hash with the one stored in the database.</p>
<h3 id="is-password-hashing-the-same-as-encryption">Is password hashing the same as encryption?<a class="heading-anchor" href="#is-password-hashing-the-same-as-encryption" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>No. Encryption is designed to be reversible, allowing the original data to be recovered with the correct key. Password hashing is a one-way process that's designed to verify passwords without ever storing or revealing the original password.</p>
<h3 id="can-password-hashes-be-reversed">Can password hashes be reversed?<a class="heading-anchor" href="#can-password-hashes-be-reversed" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>Not directly. Password hashes are designed to be one-way, meaning you can't simply reverse them to recover the original password. However, attackers can still try to guess passwords by hashing common passwords and comparing the results with stolen password hashes, which is why strong, unique passwords are so important.</p>
<h3 id="why-can-websites-reset-my-password-if-they-dont-know-it">Why can websites reset my password if they don't know it?<a class="heading-anchor" href="#why-can-websites-reset-my-password-if-they-dont-know-it" data-heading-anchor="" aria-label="Copy link to this section">#</a></h3>
<p>A password reset doesn't recover your existing password. Instead, the website generates a temporary reset token that lets you choose a new password. Once you set a new password, the old password hash is replaced with a new one.</p>]]></content:encoded>
    </item>
    <item>
      <title>What happens after you press Enter</title>
      <link>https://themissinglevel.dev/what-happens-after-you-press-enter/</link>
      <guid isPermaLink="true">https://themissinglevel.dev/what-happens-after-you-press-enter/</guid>
      <pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate>
      <description>DNS, TCP, TLS, HTML parsing, rendering: the journey from typing a URL to seeing the first pixel on screen.</description>
      <category>How the Web Works</category>
      <content:encoded><![CDATA[<p>There is a classic interview question: "what happens when you type a URL and press Enter?" For years my honest answer would have been "the page loads". I knew the acronyms, DNS and TCP and TLS, but only as names. What I was missing was the story, the actual sequence of events between the keypress and the first pixel on screen. Once I learned it, many performance problems that used to confuse me suddenly had clear explanations.</p>
<p>So here is the story, told the way I wish someone had told me. You type an address, you press Enter, and for the next few hundred milliseconds your browser works through a checklist that has not really changed in thirty years: find the server, open a connection, secure the connection, ask for the page, then turn a wall of text into pixels. Every step is a place where time can quietly disappear, and that is exactly why the steps are worth knowing.</p>
<h2 id="how-does-the-browser-find-the-server">How does the browser find the server?<a class="heading-anchor" href="#how-does-the-browser-find-the-server" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The address you typed is a name, and names mean nothing to the network. The internet moves traffic between numeric IP addresses, so before anything else, the browser has to translate <code>example.com</code> into something like <code>93.184.216.34</code>. That translation system is DNS. The simplest way to describe it is a phone book spread across thousands of servers around the world.</p>
<p>A <a href="/what-happens-during-a-dns-lookup-a-step-by-step-guide/">full DNS lookup</a> happens in several steps. Your machine asks a resolver, which is usually run by your internet provider or a public service like Google or Cloudflare. If the resolver does not already know the answer, it asks other DNS servers, level by level, until it reaches the servers responsible for that domain. The good news is that answers get cached at every step, in your browser, in your operating system and in the resolver itself, and each copy expires after a set time. A popular site usually resolves instantly from a nearby cache. An unknown site might need a real trip across the world before the browser even knows where to send the request.</p>
<h2 id="how-does-the-browser-open-a-connection">How does the browser open a connection?<a class="heading-anchor" href="#how-does-the-browser-open-a-connection" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>With an IP address in hand, the browser can finally talk to the server, but it cannot simply shout "give me the page" right away. First the two machines set up a TCP connection, and they do it with a short exchange called the three-way handshake. Your machine says "I would like to talk", the server answers "I hear you, go ahead", and your machine confirms "great, starting now". Three messages, and only then does a reliable channel exist between them.</p>
<p>This exchange sounds like something you could skip, but you cannot, because TCP is what makes the internet dependable. The network underneath drops packets, duplicates them and delivers them in the wrong order. TCP hides all of that by numbering every piece of data and resending whatever gets lost. The handshake is how the two sides agree on the starting numbers, so the counting works. The cost is one full round trip before any real data flows. This is why physical distance to the server still matters, even with a fast connection. It is also why CDNs place servers near users: you cannot make light travel faster, but you can make the trip shorter.</p>
<h2 id="how-does-https-secure-the-connection">How does HTTPS secure the connection?<a class="heading-anchor" href="#how-does-https-secure-the-connection" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>For an <code>https</code> address there is one more negotiation before the first real byte. It is called the TLS handshake, and it has two jobs. The first job is proving that you are talking to the right server. The server presents a certificate, which is a signed statement from an organization your browser already trusts, saying that this server really does speak for that domain. Your browser checks the signatures, and this check is the machinery behind every certificate warning you have ever clicked through, hopefully after reading it.</p>
<p>The second job is agreeing on encryption keys. The two sides perform a key exchange, which is clever math that lets them arrive at a shared secret even while someone watches every message between them. From that point on, everything they send is encrypted. All of this costs roughly one more round trip, and then, at last, the browser can ask for what you wanted.</p>
<h2 id="what-happens-when-the-browser-asks-for-the-page">What happens when the browser asks for the page?<a class="heading-anchor" href="#what-happens-when-the-browser-asks-for-the-page" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The request itself is very simple after all that setup. The browser sends an <a href="/seven-http-request-headers-every-developer-should-understand/">HTTP request</a>, which is plain text that says, more or less, "GET / and here is who is asking". The server replies with a status code, response headers and the HTML itself. After three phases of pure plumbing, this is the first moment the conversation is about actual content.</p>
<p>It is also the moment where <a href="/what-is-a-server-really/">the server</a> can be slow. Everything up to now was network delay, but the gap between request and response is your application: routing, <a href="/what-is-a-database/">database queries</a>, template rendering, all of it happening while the browser waits. When developers talk about server response time or "time to first byte", this pause is the thing they are measuring.</p>
<h2 id="how-does-the-browser-turn-html-into-pixels">How does the browser turn HTML into pixels?<a class="heading-anchor" href="#how-does-the-browser-turn-html-into-pixels" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>Now the browser has HTML, and the most interesting work starts. It parses the document from top to bottom, building a tree of elements called the DOM. As it reads, it keeps finding more things it needs. A stylesheet link, an image, a script tag: each one triggers another download, and some of them change how the browser behaves.</p>
<p>Two of these downloads are special because they can pause everything. CSS blocks rendering, because the browser refuses to paint anything before it knows how the page should look. A flash of unstyled content would be worse than a short wait. Classic script tags are even more disruptive, because <a href="/why-we-needed-javascript/">Javascript</a> can rewrite the document while it is being parsed. So the parser must stop, fetch the script, run it, and only then continue. This is why "put CSS early, load scripts with <code>defer</code>" became standard performance advice long ago. Fonts have their own version of this problem, where browsers hide or swap text while the real font downloads.</p>
<p>Once the browser has the DOM and the styling information, it runs layout, calculating where every element sits and how big it is. Then it paints, filling in actual pixels and combining the layers into a frame. That frame, the first one with real content in it, is the moment this whole story has been building toward. From keypress to this point is often under a second, and inside that second there was a distributed name lookup, two negotiated handshakes, an application doing real work and a rendering engine laying out a document. Every link you click after that repeats most of the journey, and that repeated cost is exactly what <a href="/why-we-needed-single-page-applications/">single page applications</a> were built to avoid.</p>
<h2 id="why-is-this-story-worth-knowing">Why is this story worth knowing?<a class="heading-anchor" href="#why-is-this-story-worth-knowing" data-heading-anchor="" aria-label="Copy link to this section">#</a></h2>
<p>The practical value is that "the site is slow" stops being one big mystery and becomes five small questions. Slow for everyone or only for far-away users? Look at network round trips and CDN placement. Long wait before the first byte? That is the server's part, so go look at the backend. Page arrives fast but appears late? Now you are hunting render-blocking CSS and scripts. Each phase leaves its own kind of evidence, and the waterfall chart in DevTools will show you exactly which one is taking the time.</p>
<p>And the next time someone asks you the interview question, you will have something better than acronyms. You will have the story: find the server, open a connection, secure it, ask for the page, then build the page from text. Everything else in web performance is a detail on top of those five steps.</p>]]></content:encoded>
    </item>
  </channel>
</rss>