<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mahmud Hasan</title>
    <description>The latest articles on DEV Community by Mahmud Hasan (@mahmudhasan).</description>
    <link>https://dev.to/mahmudhasan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195423%2F405dd7d7-0720-408f-ad1d-a186bafefd8b.png</url>
      <title>DEV Community: Mahmud Hasan</title>
      <link>https://dev.to/mahmudhasan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mahmudhasan"/>
    <language>en</language>
    <item>
      <title>Two 9.2-Rated Flaws in PHP. The Patch Lands Today at 14:00 UTC — Attackers Got a Day’s Head Start.</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Wed, 21 Oct 2026 08:32:25 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/two-92-rated-flaws-in-php-the-patch-lands-today-at-1400-utc-attackers-got-a-days-head-start-2b17</link>
      <guid>https://dev.to/mahmudhasan/two-92-rated-flaws-in-php-the-patch-lands-today-at-1400-utc-attackers-got-a-days-head-start-2b17</guid>
      <description>&lt;p&gt;Yesterday afternoon, an anonymous Hacker News post claimed — with no evidence at all, the poster admitted — that PHP had two CVSS-10 remote code execution flaws affecting everything from PHP 5 onward. Within hours, security firm watchTowr confirmed the flaws were real. By evening, PHP had pulled its release forward a full day. The patch ships today, October 21, at 14:00 UTC. This is the playbook for the hours before it lands — and the days after.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PHP actually announced (and what it didn't)
&lt;/h2&gt;

&lt;p&gt;The official notice on php.net, dated October 20, is nine sentences long. The PHP security team gave "advance notice of an upcoming security release" scheduled for Wednesday, October 21, 2026 at 14:00 UTC. It addresses two critical vulnerabilities, both rated CVSS v4.0 9.2. "All PHP users are encouraged to prepare now so they can upgrade as soon as the release is available."&lt;/p&gt;

&lt;p&gt;Then the sentence that matters most: "We reserve the right to release earlier should details of either vulnerability become public, or should evidence of active exploitation appear in the wild."&lt;/p&gt;

&lt;p&gt;What the notice does &lt;em&gt;not&lt;/em&gt; contain: CVE numbers, affected versions, or fixed version numbers. "Full details, affected versions, and the fixed versions will be published with the release." That is normal for an embargoed disclosure. What is not normal is the next part.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the embargo broke before the release
&lt;/h2&gt;

&lt;p&gt;At ~15:20 UTC on October 20, a now-deleted Hacker News post (user "petecooper") cited a "reliable source" claiming two CVSS-10 PHP RCE flaws affecting PHP 5 onward, with disclosure planned for October 22. The poster's own evidence: "source: trust me, bro."&lt;/p&gt;

&lt;p&gt;Then the story changed. watchTowr confirmed on X: "We were all supposed to wait until Thursday at 11am ET. Information has leaked wrt zero-day embargoed RCE vulns in PHP. We can confirm that they're real." Their post had a typo in the CVE IDs — "CVE-2926-109618/619" — which tells you how fast this moved. The corrected IDs: CVE-2026-109618 and CVE-2026-109619.&lt;/p&gt;

&lt;p&gt;watchTowr CEO Benjamin Harris told BleepingComputer the flaws were leaked "in the last hour," had been embargoed for a Thursday 11 AM Eastern release, and were remote code execution flaws. The company believes it reproduced CVE-2026-109618 in its own lab — but is withholding technical findings until the patches ship. Crucially: watchTowr reported no exploitation in the wild at the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we know about the flaws — and the evidence boundary
&lt;/h2&gt;

&lt;p&gt;PHP has confirmed the severity and the schedule. It has confirmed nothing else. Everything below is third-party attribution, held at that distance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The CVE IDs are watchTowr-attributed, not official.&lt;/strong&gt; PHP's notice names no CVEs. Both entries were still "reserved" on NVD with no description as of this morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9.2 vs 10.0.&lt;/strong&gt; PHP rates the flaws CVSS v4.0 9.2. The leak and watchTowr say "CVSS 10.0" without naming which scoring version. Treat 9.2 as the number and the 10 as the rumor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"PHP 5 onward" is unconfirmed.&lt;/strong&gt; That scope came from the anonymous HN post, repeated by watchTowr's CEO — but watchTowr's own FAQ says the affected range "has not been confirmed by an official vendor advisory." Researcher Florian Roth notes the flaw is in PHP core and that only the PHP 8 branches can receive patches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mechanism is third-party speculation.&lt;/strong&gt; External research describes an unauthenticated RCE path in PHP's multipart upload processing; other coverage points at session upload-progress handling, with one suggested interim mitigation: &lt;code&gt;session.upload_progress.enabled=Off&lt;/code&gt;. But as DeXpose flags: "PHP has confirmed the severity and release schedule, not the full technical mechanism."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An exploit tool is already being named.&lt;/strong&gt; Sansec reported a screenshot of a tool called "PHP2Shell," credited to Calif.IO, circulating with the leak; its banner claims coverage from PHP 5.4.0 onward. Sansec predicts mass attacks could begin within roughly 15 minutes of the release. The "PHP2Shell" nickname is community shorthand, not PHP's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On scale: only 8.2, 8.3, 8.4, and 8.5 can receive an upstream fix (8.1 went EOL December 2025; 8.2's own security support ends this December). Mallory counted roughly 884,000 internet-facing hosts whose banners reported PHP 8.1 or older to Shodan on October 20 — 59% of versioned hosts — plus ~740,000 more that reported PHP with no version. Those are banner reports, not confirmed vulnerable systems, but they show how much of the web runs PHP it can't patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do before 14:00 UTC, in the order that matters
&lt;/h2&gt;

&lt;p&gt;The patch lands today at 14:00 UTC — 10 AM Eastern, 4 PM Central European, 8 PM in Dhaka. The window between now and then is for preparation, not panic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Find every PHP on your network.&lt;/strong&gt; Not just the app servers: cPanel/Plesk builds, containers, appliances, CI runners, dev stacks, the old WordPress site on the shared host you forgot about. &lt;code&gt;php -v&lt;/code&gt; everywhere. Most teams discover at least one PHP they didn't know was running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Know how your fix arrives.&lt;/strong&gt; Distro packages (check whether your distro backports security fixes), panel builds (cPanel/Plesk ship their own PHP — their timeline is the one that matters), source builds (have your exact configure line ready), or managed hosting (ask your host now; several have already posted status pages saying they'll handle it).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Snapshot and back up first.&lt;/strong&gt; PHP patch releases are usually safe, but extensions and pinned builds can break in odd ways. A snapshot is cheaper than a rollback at midnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Patch internet-facing systems first, in order of exposure.&lt;/strong&gt; Public web servers, then internal apps, then everything else. Wordfence's threat team says it will assess firewall coverage as soon as the advisory drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. If you're on 5.x, 7.x, or 8.1: no patch is coming for you.&lt;/strong&gt; CERT-EU's guidance is blunt: isolate internet-facing apps on EOL PHP from public networks. If upgrading today is impossible, a WAF and network isolation are your only defenses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Watch for the weaponization.&lt;/strong&gt; Look for php-fpm, php-cgi, or Apache worker processes spawning shells, download tools, or interpreters. The precedent: when PHP shipped an out-of-cycle fix for CVE-2024-4577 (the Windows CGI RCE) in June 2024, Akamai's honeypots caught exploit attempts within 24 hours of disclosure. Sansec predicts ~15 minutes this time, because attackers already know the patch is coming today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the next 48 hours matter
&lt;/h2&gt;

&lt;p&gt;Is this fear-mongering? PHP says 9.2, watchTowr reports no exploitation in the wild, and the "CVSS 10 / PHP 5 onward" claims come from a leak whose poster admitted having no evidence.&lt;/p&gt;

&lt;p&gt;No — because the leak inverted the normal disclosure timeline. Usually defenders get the patch and the details at the same moment attackers do. Here, attackers got a day's head start of &lt;em&gt;awareness&lt;/em&gt;: everyone now knows a PHP core RCE patch drops at 14:00 UTC today, and tooling is already being named in screenshots. The June 2024 precedent says the gap between "patch available" and "exploit circulating" closes within a day. Preparation today costs almost nothing. A web shell tomorrow costs everything.&lt;/p&gt;

&lt;p&gt;One honest caveat: this post publishes before the release, so the fixed versions, affected-versions matrix, and technical details all arrive at 14:00 UTC. Check the official advisory against everything above — the advisory wins, always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; 14:00 UTC today. Know where PHP runs in your stack, know how your fix arrives, snapshot, and patch the internet-facing systems first. The embargo already broke once — don't let your own patching be the second thing that runs late.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/archive/2026.php#2026-10-20-1" rel="noopener noreferrer"&gt;PHP: Upcoming Critical PHP Security Release (php.net news archive, Oct 20, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openwall.com/lists/oss-security/2026/10/20/34" rel="noopener noreferrer"&gt;oss-security: Upcoming Critical PHP Security Release (mailing-list mirror, Oct 20, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.wordfence.com/blog/2026/10/psa-upcoming-critical-php-security-release/" rel="noopener noreferrer"&gt;Wordfence: PSA — Upcoming Critical PHP Security Release (Oct 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dexpose.io/php2shell-php-rce-exposure-response/" rel="noopener noreferrer"&gt;DeXpose: PHP2Shell — How Security Teams Should Assess and Contain the Reported PHP RCEs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phoenix.security/php-core-rce-cve-2026-109618-109619-critical-remote-code-execution/" rel="noopener noreferrer"&gt;Phoenix Security: PHP Core RCE CVE-2026-109618/109619&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://presswizards.com/critical-php-vulnerabilities-what-website-owners-need-to-know-and-do-now/" rel="noopener noreferrer"&gt;PressWizards: Critical PHP Vulnerabilities — What Website Owners Need to Know and Do Now&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://windowsforum.com/news/php-critical-security-release-set-for-october-21-prepare-iis-windows-server-and-legacy-installs.449300/" rel="noopener noreferrer"&gt;WindowsForum: PHP Critical Security Release Set for October 21&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Implement Rate Limiting for an API: In the Order That Actually Matters</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:31:32 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/how-to-implement-rate-limiting-for-an-api-in-the-order-that-actually-matters-3a1</link>
      <guid>https://dev.to/mahmudhasan/how-to-implement-rate-limiting-for-an-api-in-the-order-that-actually-matters-3a1</guid>
      <description>&lt;p&gt;The request that takes your API down rarely comes from a hacker. It comes from a legitimate customer with a retry loop, or a scraper that found your endpoint. Rate limiting is how you stay up — but most tutorials teach the happy path and skip the ways production limiters fail. This is the order that works: pick the algorithm for your bottleneck, identify clients honestly, tier by endpoint cost, return useful 429s, and plan for the limiter's own failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, pick the algorithm for your bottleneck
&lt;/h2&gt;

&lt;p&gt;Four serious algorithms for four different bottlenecks — pick the one that matches yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed window&lt;/strong&gt; is the simplest: one counter per client per window, reset at the window's end. Cheap and fine for internal APIs — with a known leak at the window edge: 100 requests at 12:00:59 plus 100 at 12:01:00 is 200 requests in two seconds, fully "compliant."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window log&lt;/strong&gt; records every request timestamp and counts what falls inside the trailing window. Perfect accuracy, no edge leak — at the price of one stored entry per request per client. Reserve it for logins, MFA codes, password resets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token bucket&lt;/strong&gt; holds up to N tokens, refilled at a fixed rate; each request costs a token, expensive endpoints cost ten. It absorbs bursts up to the bucket's capacity while enforcing the average rate — the most deployed algorithm in production. AWS API Gateway throttles with token buckets (10,000 requests/second steady-state plus a 5,000-request burst); Stripe's engineering writeup describes a per-user token bucket with burst allowance above the cap for flash sales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leaky bucket&lt;/strong&gt; is the mirror image: requests queue and drain at a constant rate, so output is perfectly smooth. Choose it when your downstream can't absorb a burst — payment rails, legacy mainframes, anything where "eventually, in order" beats "fast."&lt;/p&gt;

&lt;p&gt;Choose by what sits behind your API: if it can't absorb a burst, use a leaky bucket; if you're rate-limiting logins, use a sliding window log; otherwise use a token bucket. Fixed windows are for internal tools, not the public internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key the limiter on the right identity
&lt;/h2&gt;

&lt;p&gt;The algorithm is the part everyone writes about. The identity is the part that breaks.&lt;/p&gt;

&lt;p&gt;Key by user ID when authenticated, IP when not. In Laravel's named limiters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Cache\RateLimiting\Limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\RateLimiter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two traps in that last line. First, behind a load balancer or CDN, &lt;code&gt;$request-&amp;gt;ip()&lt;/code&gt; may resolve to the proxy's address — one shared budget for everyone. If a CDN terminates the connection, key on its signed header (Cloudflare's &lt;code&gt;CF-Connecting-IP&lt;/code&gt;). Second, the naive fix is trusting &lt;code&gt;X-Forwarded-For&lt;/code&gt;, and that header is client-controlled: one curl flag hands an attacker a fresh budget per request. Only trust forwarded headers from proxies you control — in Laravel, configure trusted proxies and ignore the header on direct connections.&lt;/p&gt;

&lt;p&gt;And the mistake that silently starves paying users: one global bucket for everything. Without &lt;code&gt;-&amp;gt;by()&lt;/code&gt;, one noisy scraper throttles your whole user base. Segment per user, and give anonymous traffic a smaller bucket than authenticated traffic. (On IPv6, group by subnet rather than keying on the full /128 — one user can rotate through an enormous address space; Traefik documents an &lt;code&gt;ipv6Subnet&lt;/code&gt; option for exactly this.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier by endpoint cost, not just by user
&lt;/h2&gt;

&lt;p&gt;Sixty requests per minute is reasonable for fetching a profile; catastrophic for an endpoint that sends an SMS. Price rate limits in the currency of your infrastructure: expensive endpoints get stricter budgets, or cost more tokens in the same bucket.&lt;/p&gt;

&lt;p&gt;Stripe documents it explicitly: a global 100 requests/second in live mode, 25 per individual endpoint, plus resource-specific caps. When you hit one, Stripe's 429 tells you which tier fired via the &lt;code&gt;Stripe-Rate-Limited-Reason&lt;/code&gt; header. Copy that: the error names the rule that fired.&lt;/p&gt;

&lt;p&gt;Five hundred reads a minute, three exports a minute.&lt;/p&gt;

&lt;p&gt;The cheapest version is multiple limits per route — Laravel supports it out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reports'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'minute:'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'exports:'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five hundred reads a minute, three exports a minute — price the limit in the cost of the endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return a 429 that helps instead of hurting
&lt;/h2&gt;

&lt;p&gt;A rate limiter that returns a bare 429 with no context is a locked door with no sign — the client retries immediately, in a loop, making your load problem worse. Four headers and a paragraph of documentation fix it.&lt;/p&gt;

&lt;p&gt;Status code is &lt;code&gt;429 Too Many Requests&lt;/code&gt; (RFC 6585). Pair it with &lt;code&gt;Retry-After&lt;/code&gt;, in seconds: &lt;code&gt;Retry-After: 12&lt;/code&gt;. Subtlety from Cloudflare's feedback to the IETF: under token or leaky bucket schemes, the next request may be permitted &lt;em&gt;before&lt;/em&gt; the window fully resets — so don't blindly echo the window reset.&lt;/p&gt;

&lt;p&gt;For limit metadata, pick one of two conventions. The de-facto standard is &lt;code&gt;X-RateLimit-Limit&lt;/code&gt; / &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; / &lt;code&gt;X-RateLimit-Reset&lt;/code&gt;, used by GitHub and Laravel's throttle middleware. The newer convention is the IETF's &lt;code&gt;RateLimit-Policy&lt;/code&gt; / &lt;code&gt;RateLimit&lt;/code&gt; fields — but as of October 2026 it's still a draft, not an RFC. What breaks developers is mixing conventions or shipping none. Document yours with an example 429 response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four ways production limiters actually die
&lt;/h2&gt;

&lt;p&gt;Run your implementation through this list before you ship it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Retry storms.&lt;/strong&gt; The clients you rate-limited retry immediately, all at once, synchronized — the thundering herd — and one 429 wave becomes a self-inflicted DDoS. Publish &lt;code&gt;Retry-After&lt;/code&gt;, but more importantly make &lt;em&gt;your&lt;/em&gt; clients behave: exponential backoff with jitter, which Stripe's docs recommend for exactly this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Redis is down and nobody decided what happens next.&lt;/strong&gt; Your limiter lives in Redis, and Redis will eventually be unreachable. Fail &lt;em&gt;open&lt;/em&gt; for checkout: blocking all purchases over a cache hiccup is a business disaster. Fail &lt;em&gt;closed&lt;/em&gt; for login and OTP endpoints: an attacker would love a password-guessing window with no limiter. Decide per route and test it by killing Redis in staging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You forgot concurrency exists.&lt;/strong&gt; Rate limits count requests per window, not simultaneous requests. Stripe maintains a separate &lt;em&gt;global concurrency limit&lt;/em&gt; — 50 in-progress requests by default in live mode — and its 429s distinguish &lt;code&gt;global-concurrency&lt;/code&gt; from &lt;code&gt;global-rate&lt;/code&gt;. If your endpoints are slow, cap concurrency too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Your check-and-record isn't atomic.&lt;/strong&gt; A fixed-window check that INCRs, reads the count, then decides in application code races when two gateway nodes hit the last slot at the same instant. Run the whole thing as a Lua script — Redis executes Lua atomically — or document that your limit is approximate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do on Monday
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wire up a limiter you don't have to babysit.&lt;/strong&gt; Laravel named limiter + &lt;code&gt;throttle:&lt;/code&gt; middleware, &lt;code&gt;express-rate-limit&lt;/code&gt;, or Flask-Limiter — backed by Redis in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key honestly.&lt;/strong&gt; User ID when authenticated, IP when not, from a proxy chain you control. IPv6 goes in subnet buckets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier by cost.&lt;/strong&gt; Generous for reads, strict for login, signup, exports, webhooks. Let the 429 name the tier that fired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship useful 429s.&lt;/strong&gt; &lt;code&gt;429&lt;/code&gt; + &lt;code&gt;Retry-After&lt;/code&gt; in seconds + one consistent header set, documented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the failure modes.&lt;/strong&gt; Kill Redis in staging; confirm checkout stays up while login stays guarded. Hammer staging and watch whether clients back off or stampede.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Laravel 12.x docs — rate limiting (named limiters, Redis throttle, response-based limits): &lt;a href="https://laravel.com/index.php/framework/docs/12.x/routing" rel="noopener noreferrer"&gt;https://laravel.com/index.php/framework/docs/12.x/routing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Stripe docs — rate limits (100 rps live, 25 rps per endpoint, backoff guidance): &lt;a href="https://docs.stripe.com/rate-limits" rel="noopener noreferrer"&gt;https://docs.stripe.com/rate-limits&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Stripe blog — Scaling your API with rate limiters: &lt;a href="https://stripe.com/en-fi/blog/rate-limiters" rel="noopener noreferrer"&gt;https://stripe.com/en-fi/blog/rate-limiters&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub docs — rate limits for the REST API: &lt;a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api" rel="noopener noreferrer"&gt;https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS docs — API Gateway request throttling (token bucket): &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare docs — WAF rate limiting rules: &lt;a href="https://developers.cloudflare.com/waf/account/rate-limiting-rulesets/create-api/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/waf/account/rate-limiting-rulesets/create-api/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;IETF draft — RateLimit header fields for HTTP (&lt;code&gt;draft-ietf-httpapi-ratelimit-headers&lt;/code&gt;): &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SharpByte — designing a rate limiter: &lt;a href="https://www.sharpbyte.dev/system-design/design-rate-limiter" rel="noopener noreferrer"&gt;https://www.sharpbyte.dev/system-design/design-rate-limiter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CodingPancake — distributed rate limiting at scale: &lt;a href="https://www.codingpancake.com/2026/09/distributed-rate-limiting-at-scale.html" rel="noopener noreferrer"&gt;https://www.codingpancake.com/2026/09/distributed-rate-limiting-at-scale.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Traefik docs — rate limit middleware (IPv6 subnet grouping): &lt;a href="https://doc.traefik.io/traefik-hub/api-gateway/reference/routing/http/middlewares/ref-rate-limit" rel="noopener noreferrer"&gt;https://doc.traefik.io/traefik-hub/api-gateway/reference/routing/http/middlewares/ref-rate-limit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Do a Good Code Review: Small Diffs, Fast Reviews</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 22:57:11 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/how-to-do-a-good-code-review-small-diffs-fast-reviews-1a8p</link>
      <guid>https://dev.to/mahmudhasan/how-to-do-a-good-code-review-small-diffs-fast-reviews-1a8p</guid>
      <description>&lt;p&gt;Most code reviews are theater. A thousand-line pull request lands on Friday, sits untouched for four days, and merges on Monday with two approvals from people who skimmed it. Nobody found a bug, nobody learned anything — the diff was too big to understand, so the approval was a ritual, not a review. Google's engineering docs and the peer-review research agree on the uncomfortable point: reviews only work when the diff is small and the feedback is fast. Everything else is technique.&lt;/p&gt;

&lt;p&gt;I've spent the week reading Google's published review docs, SmartBear's peer-review benchmarks, and the best practitioner write-ups I could find. The advice below is what survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Know what the review is actually for
&lt;/h2&gt;

&lt;p&gt;A good review does three things: it catches the mistakes the author missed, it builds a shared understanding of what changed and why, and it keeps the codebase's abstractions consistent — pointing out that a helper already exists elsewhere so the author doesn't reinvent it. That's it. One practitioner puts it even more bluntly: the goal should be creating understanding, never forcing your colleague to justify themselves.&lt;/p&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; on the list: perfection. Google's standard — approve once a change &lt;em&gt;definitely improves&lt;/em&gt; code health, even if it isn't perfect. Perfect code doesn't exist; "better" is enough. If more feedback would make significant improvements, ask. Otherwise, approve and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep the diff small — the only rule with numbers
&lt;/h2&gt;

&lt;p&gt;This is the most quantified piece of the whole discipline, and almost nobody follows it. SmartBear's peer-review guidance recommends reviewing fewer than 400 lines of code at a time, inspecting under 500 lines per hour, and limiting concentrated review sessions to 60 minutes. Their reported benchmark: a 200–400 line review conducted over 60–90 minutes can yield 70–90% defect discovery. That's not a guarantee for every codebase — it's a directional benchmark — but the shape of it is the point: small reviews find things; huge ones don't.&lt;/p&gt;

&lt;p&gt;The reviewer can't fix a 1,000-line diff — the &lt;em&gt;author&lt;/em&gt; owns reviewability. Google splits work into small change lists: ten 100-line CLs beat one 1,000-line CL, every time. If a change is too big to understand in one pass, split it by independently testable behavior before asking for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Review fast — slow reviews rot everything
&lt;/h2&gt;

&lt;p&gt;Google's maximum for a review is one business day. Why the urgency? Three compounding costs. First, the review becomes a blocker: the author moves on, new work piles up, and days become weeks. Second, developers get frustrated — and frustrated developers submit worse code and do less of the unglamorous cleanup work ("if my reviewer won't reply for four days, what's the point?"). Third, code quality degrades precisely where you thought reviews were protecting it.&lt;/p&gt;

&lt;p&gt;The real performance metric isn't comment count — it's time-to-first-response. If your team measures one review metric, make it that one. And if you spot an architectural problem early, say so immediately, before finishing the rest: Google tells reviewers to send severe findings the moment they land. The author may already be building on top of what you're about to reject.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ask questions, don't deliver verdicts
&lt;/h2&gt;

&lt;p&gt;Write questions, not pronouncements. "Is there a reason this migration isn't reversible?" lands differently than "make this reversible" — and the question often wins, because suboptimal solutions are usually carefully chosen compromises the reviewer can't see. Don't understand a section? Ask what it does. Disagree? Ask why, instead of criticizing. See a likely mistake? Ask whether section X might lead to problem Y.&lt;/p&gt;

&lt;p&gt;The one test that tells you whether you're doing this honestly: if the question you're about to write has only one possible answer, it's not a question — it's a demand in disguise. Say what you mean instead. Reserve the direct version for the rare case where you're certain a solution must not be merged as written.&lt;/p&gt;

&lt;p&gt;This is also the answer to the question every junior dev asks: &lt;em&gt;am I even allowed to review senior engineers' code?&lt;/em&gt; Yes — because asking comprehension questions &lt;em&gt;is&lt;/em&gt; reviewing. A less experienced reviewer finds fewer subtle defects but more than makes up for it by forcing the author to explain and re-examine their own decisions. If you're new and lost, ask the author to walk you through the change — that's not a weak review, it's the review working as designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Label your comments so the author knows what's blocking
&lt;/h2&gt;

&lt;p&gt;The biggest cause of review friction isn't disagreement — it's ambiguity. Fix it with labels. Four is enough: &lt;strong&gt;Blocking&lt;/strong&gt; (concrete defect, security issue, broken requirement — nothing merges until fixed), &lt;strong&gt;Suggestion&lt;/strong&gt; (a better approach exists; the author picks the fix), &lt;strong&gt;Question&lt;/strong&gt; (you need context before judging), and &lt;strong&gt;Nit&lt;/strong&gt; (optional polish that must not delay the merge).&lt;/p&gt;

&lt;p&gt;Google's version is simpler — prefix non-essential polish with "Nit:" — and the rule behind it is absolute: never block a PR on personal style preference, and never mix style changes into a real change. Formatters exist; your taste is not a standard. Critique the code and its consequences, not the person: "this condition permits duplicate writes" is reviewable evidence; "you forgot idempotency" is needlessly personal. And when a comment gets addressed — or you spot something genuinely clever — say so. Reviews skew toward problems; one acknowledgment costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Read in the right order: intent first, lines last
&lt;/h2&gt;

&lt;p&gt;Don't start with line-level style. Google's sequence: get an overview of the change, examine the main parts, then go file by file in a sensible order. In practice: read the PR description first (state the expected behavior in one sentence), trace the changed behavior — the main path plus at least one failure path — then design, then tests, and only then the diff.&lt;/p&gt;

&lt;p&gt;At each level, per Google's checklist: design (does this belong here?), functionality (does it behave as intended?), complexity (could it be simpler?), tests (would they fail if the code broke?), naming, comments (explaining &lt;em&gt;why&lt;/em&gt;, not &lt;em&gt;what&lt;/em&gt;), consistency. And read the context around changed lines, not just the diff — two new &lt;code&gt;if&lt;/code&gt; blocks look harmless until you notice they join an &lt;code&gt;if-else&lt;/code&gt; chain that already had fifteen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know when to stop
&lt;/h2&gt;

&lt;p&gt;The thoughtful reviewer's failure mode is the endless review. Google's advice — &lt;em&gt;find an end&lt;/em&gt; — is kind: dragging a review through round after round is soul-deadening. "LGTM" doesn't mean "this will never fail"; it means "looks good to me." If it looks good, move on; big refactorings you spot belong in a new change.&lt;/p&gt;

&lt;p&gt;The one exception that proves the rule: never accept "I'll fix it in a follow-up." Follow-ups rarely happen — not from malice, but because priorities shift and queues swallow them. There's no benefit in merging code you immediately have to fix; you're just adding technical debt that might be forgotten. Insist the work happens now. (Emergencies are the exception: when production is down, quality standards relax and the review becomes the reviewer's first priority.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;p&gt;Pick one pull request this week and review it within a day. Ask one genuine "why" instead of issuing one correction. And when you open a 900-line diff, send it back with one sentence: split this by independently testable behavior. Small, fast, and questioned — that's the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.freecodecamp.org/news/what-google-taught-me-about-code-reviews/" rel="noopener noreferrer"&gt;How Google Does Code Reviews – freeCodeCamp (summary of Google's engineering practices)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anygen.io/showcase/code-review/index.html" rel="noopener noreferrer"&gt;Code Review: Process, Checklist  Examples – AnyGen (SmartBear benchmarks, Google review taxonomy)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://farbenmeer.de/en/blog/how-to-code-review" rel="noopener noreferrer"&gt;How to write good code reviews – farbenmeer.de (the questions-only method)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://graphite.dev/guides/what-makes-a-good-code-review" rel="noopener noreferrer"&gt;What makes a good code review – Graphite (clarity, timeliness, knowledge sharing)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Related: &lt;a href="https://www.mahmudhasan.pro/blog/how-to-write-agents-md-file" rel="noopener noreferrer"&gt;How to Write an AGENTS.md File Your Coding Agent Will Actually Follow&lt;/a&gt; · &lt;a href="https://www.mahmudhasan.pro/blog/git-stash-not-working-four-fixes" rel="noopener noreferrer"&gt;Git Stash Not Working? The Four Fixes Nobody Teaches You&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Remember Names of People You Meet: The Science-Backed Fix</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 22:46:44 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/how-to-remember-names-of-people-you-meet-the-science-backed-fix-4jje</link>
      <guid>https://dev.to/mahmudhasan/how-to-remember-names-of-people-you-meet-the-science-backed-fix-4jje</guid>
      <description>&lt;h2&gt;
  
  
  The name never makes it in
&lt;/h2&gt;

&lt;p&gt;Someone extends a hand. "Hi, I'm —" and then the name, and then you're already mid-handshake, choosing between "nice to meet you" and "pleasure." Ninety seconds later, someone asks you to introduce them and the name is gone. Not misfiled. Gone.&lt;/p&gt;

&lt;p&gt;Psychologists gave this a name in 1973: the &lt;strong&gt;next-in-line effect&lt;/strong&gt;. Malcolm Brenner sat 88 undergraduates in a square, had them read words aloud in turn, and found the words people remembered worst were the ones spoken in the nine seconds before their own turn. Those words weren't forgotten — they were never learned. Every reader was busy rehearsing their own word while the others spoke.&lt;/p&gt;

&lt;p&gt;An introduction is the next-in-line effect in its purest form: their name lands while your brain is composing your greeting, aiming your eye contact, running the etiquette checklist — the room is closed for renovations.&lt;/p&gt;

&lt;p&gt;The twist that makes this fixable: in 1985, Charles Bond re-ran the experiment to settle whether the effect is an encoding problem or a retrieval problem. Half of his 144 students were told &lt;em&gt;beforehand&lt;/em&gt; to make a special effort to remember what happened before their turn — and that group reversed the deficit entirely. Deciding to remember, before the introductions start, is the single move the experiments prove works. Not effort afterward. Intent before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The name has nowhere to stick
&lt;/h2&gt;

&lt;p&gt;Even when the name gets in, your brain treats it badly. University of Florida psychological scientists Lise Abrams and Danielle K. Davis wrote that "the ability to learn and remember proper names, particularly people's names, is notoriously more difficult relative to other types of words." Their explanation is brutally simple: a name is a meaningless label. It reveals nothing about the person it belongs to.&lt;/p&gt;

&lt;p&gt;The classic demonstration is the &lt;strong&gt;Baker-baker paradox&lt;/strong&gt;. Tell a group of volunteers that a man in a photo is a baker, and they remember it weeks later. Tell another group the man's &lt;em&gt;name&lt;/em&gt; is Baker, and almost nobody does. "Baker" the job arrives with a network of flour, aprons, and early mornings. "Baker" the name arrives as a noise. Your memory holds meaning; a name has none.&lt;/p&gt;

&lt;p&gt;It gets worse on the way back out. A name usually belongs to exactly one person, while "runs a bar" is true of hundreds of people you've met — so the fact has hundreds of connections feeding it and the name has one. Fewer roads in, fewer roads back out. And your face is doing all of this with dedicated hardware: in 1997, Nancy Kanwisher's team found a patch of brain about the size of a blueberry that fires for faces and stays quiet for everything else. Faces get processed in a fraction of a second. The name queues up behind them as a plain sound with nothing attached. Then there's the crowd problem: two similar names compete for one slot — the Moses illusion (most people say Moses took two animals on the ark; Noah sailed it). Two Rameshs in one room will do the same to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pressure makes it worse
&lt;/h2&gt;

&lt;p&gt;Trying harder is actively counterproductive. Social pressure releases cortisol, and cortisol disrupts the conversation between your hippocampus — where memories get indexed — and your prefrontal cortex, where you do your thinking. The harder you strain to force a name to the surface, the more your retrieval pathways lock up. That's why the name you couldn't find all evening arrives on the drive home, when you've stopped trying.&lt;/p&gt;

&lt;p&gt;One reassurance: this is not your brain declining. Longitudinal studies find name-forgetting is often the first thing to change, sometimes as early as your thirties, in perfectly healthy brains — a processing-speed issue, not memory loss. In pathological conditions like Alzheimer's, people lose the &lt;em&gt;meaning&lt;/em&gt; of words: forgetting what a watch is. Forgetting what "Sarah" is called is just a normal part of being human.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-move system
&lt;/h2&gt;

&lt;p&gt;Knowing the machinery is nice, but you need the moves. Here's the system, in the order that matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Decide before the handshakes start.&lt;/strong&gt; This is Bond's 1985 finding turned into habit. Walking into a room where you'll meet people, give your brain one instruction: &lt;em&gt;I'm going to learn these names.&lt;/em&gt; That pre-commitment is what reversed the next-in-line deficit in the lab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Say it back immediately.&lt;/strong&gt; "Nice to meet you, Priya." This forces you to actually process the sound instead of letting it wash by, and it buys you a repetition. If the internal dialogue was already running when the name came, ask right then: "Sorry, was that Priya?" People read the effort as care, not clumsiness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Give it meaning.&lt;/strong&gt; This is the direct counter to the Baker-baker paradox: your brain wants hooks, so manufacture them. Link the name to something you already know — &lt;em&gt;Seymour, like my Uncle Seymour. Robin, like the bird. Florence, like the city.&lt;/em&gt; The more visual the link, the better: one classic study found people who built mental images recalled twice as many words as people who just repeated them. If the name won't bend into an image, use the mediator trick researchers verified in the lab: learn one fact about the person, link the name to the fact, and let the fact carry the name to the face. &lt;em&gt;Nadia — Nadia who rock-climbs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Use it twice in conversation.&lt;/strong&gt; "So Nadia, how do you know the host?" Then once more when you say goodbye. Each use is a rehearsal; three uses in the first few minutes is the difference between a name that evaporates and one that survives the evening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Write it down within the hour.&lt;/strong&gt; The moment you can, put the name in your phone with one note — where you met, what they do, one detail from the conversation. Review it before bed and again before the next time you'll see them. This is the only step memory competitors would call mandatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you still forget (and you will)
&lt;/h2&gt;

&lt;p&gt;First, stop trying. The information is in there and your straining is what's blocking it — shift the conversation, come back to it, and let the background processes do the work.&lt;/p&gt;

&lt;p&gt;Second, ask. The graceful version is early and specific: "I'm terrible with names at parties — can you tell me yours one more time so I get it right?" Nobody is insulted by being asked. They're insulted by being called the wrong name for six months. Practice on name-tag people — baristas, receptionists, the checkout line: low stakes, real repetition.&lt;/p&gt;

&lt;p&gt;And the counter-argument you were already forming: &lt;em&gt;but some people are just good with names.&lt;/em&gt; Consider Joan Crawford's confession to her biographer: "I remember hundreds of names, maybe more, not because it comes naturally to me, quite the opposite. I don't think I was naturally good at remembering names, but it seemed right to make the effort."&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;p&gt;Pick one upcoming event — a standup, a dinner, a client call — and do only steps one and two: decide before, say it back. That's the minimum viable version, and it's the two steps with the strongest experimental backing. Add steps three and four when they feel natural.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.psychologicalscience.org/publications/observer/obsonline/why-its-so-hard-to-remember-peoples-names.html" rel="noopener noreferrer"&gt;Why It's So Hard to Remember People's Names — Association for Psychological Science&lt;/a&gt; (Abrams  Davis, 2017)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://m.economictimes.com/news/international/us/do-you-always-forget-names-psychologists-explain-why-its-almost-never-a-memory-problem/amp_articleshow/127764731.cms" rel="noopener noreferrer"&gt;Do You Always Forget Names? Psychologists Explain Why It's Almost Never a Memory Problem — The Economic Times&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sciencealert.com/watch-the-science-behind-why-it-s-so-hard-to-remember-names" rel="noopener noreferrer"&gt;The Science Behind Why It's So Hard to Remember Names — ScienceAlert&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://psycnet.apa.org/record/1974-02391-001" rel="noopener noreferrer"&gt;Brenner, M. (1973). The next-in-line effect. Journal of Verbal Learning  Verbal Behavior, 12(3), 320–323.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://psycnet.apa.org/doi/10.1037/0022-3514.48.4.853" rel="noopener noreferrer"&gt;Bond, C. F. (1985). The next-in-line effect: Encoding or retrieval deficit? Journal of Personality and Social Psychology, 48(4), 853–862.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.smith.edu/news-events/news/remember-anyones-name" rel="noopener noreferrer"&gt;Remember Anyone's Name — Smith College&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.researchgate.net/publication/229959619_An_imagery_mnemonic_for_the_learning_of_people's_names" rel="noopener noreferrer"&gt;Naveh-Benjamin et al. An imagery mnemonic for the learning of people's names — ResearchGate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getlevelheaded.com/blogs/remembernames" rel="noopener noreferrer"&gt;Want to Be Great at Remembering Names? — getlevelheaded&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>programming</category>
      <category>devdiscuss</category>
    </item>
    <item>
      <title>How to Explain Technical Debt to Non-Technical Stakeholders</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:36:24 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/how-to-explain-technical-debt-to-non-technical-stakeholders-4gl0</link>
      <guid>https://dev.to/mahmudhasan/how-to-explain-technical-debt-to-non-technical-stakeholders-4gl0</guid>
      <description>&lt;p&gt;Here's an irony that might fix your next sprint planning meeting: the phrase "technical debt" was coined to explain a software problem to non-technical business people. Ward Cunningham invented it in 1992 while building the WyCash portfolio management system — because he had to tell his non-technical stockholders why the team needed to rework code that wasn't broken. So if you're reading a guide on how to explain technical debt to non-technical stakeholders, here's the liberating part: you're not using a bad metaphor. You're probably using it backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phrase was invented for business people
&lt;/h2&gt;

&lt;p&gt;Cunningham wrote about it in an experience report for the OOPSLA conference, "The WyCash Portfolio Management System" — his team's understanding of the financial domain had evolved, and the working code no longer reflected what they knew. So he reached for a financial metaphor, because business people already understand debt.&lt;/p&gt;

&lt;p&gt;His formulation: "Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite. The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt."&lt;/p&gt;

&lt;p&gt;Seventeen years later, Cunningham recorded a video to correct the record, because the industry had twisted his metaphor into something he never meant. "A lot of bloggers have explained the debt metaphor and confused it with the idea that you could write code poorly with the intention of doing a good job later," he said. "I'm never in favor of writing code poorly."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your usual explanation bounces off
&lt;/h2&gt;

&lt;p&gt;The typical pitch — "the codebase has a lot of technical debt, we need a sprint to refactor" — lands as "engineers want a week to polish something customers will never see." No numbers, no risk, no date. Of course it bounces off.&lt;/p&gt;

&lt;p&gt;Three mistakes make it worse. First, mixing up &lt;em&gt;debt&lt;/em&gt; with &lt;em&gt;mess&lt;/em&gt;. Robert C. Martin was blunt: "A mess is never rational, is always based on laziness and unprofessionalism." Sloppy code isn't technical debt; it's just sloppy code. Catching it early is what &lt;a href="https://dev.to/blog/how-to-do-a-good-code-review"&gt;good code reviews&lt;/a&gt; are for.&lt;/p&gt;

&lt;p&gt;Second, code-quality framing. Executives don't budget for "elegance." Steve McConnell's rule: discuss debt in money, not features — "40% of our current RD budget is going into supporting previous releases," or "We're spending $2.3 million per year servicing our technical debt."&lt;/p&gt;

&lt;p&gt;Third, treating all debt as equally bad. Fowler's Technical Debt Quadrant puts it on two axes — deliberate vs. inadvertent, reckless vs. prudent — and the corners tell the story: &lt;strong&gt;reckless-deliberate:&lt;/strong&gt; "We don't have time for design." &lt;strong&gt;Reckless-inadvertent:&lt;/strong&gt; "What's layering?" &lt;strong&gt;Prudent-deliberate:&lt;/strong&gt; "We must ship now and deal with the consequences." &lt;strong&gt;Prudent-inadvertent:&lt;/strong&gt; "Now we know how we should have done it." Fowler's summary: "The useful distinction isn't between debt or non-debt, but between prudent and reckless debt." The conversation isn't "stop borrowing." It's "know the interest rate, and pay it down on schedule."&lt;/p&gt;

&lt;h2&gt;
  
  
  The insight that changes the conversation
&lt;/h2&gt;

&lt;p&gt;Fowler added one refinement to the metaphor that most people miss — and it's the single most useful sentence you can say in the meeting. Unlike a bank loan, technical debt doesn't charge interest with the passage of time. It charges interest &lt;em&gt;by contact&lt;/em&gt;. "I only trigger an interest payment when I have to work with that part of the software," Fowler wrote. "So crufty but stable areas of code can be left alone."&lt;/p&gt;

&lt;p&gt;That flips the usual argument. "We have a lot of technical debt" is a meaningless statement without a map of where the interest gets charged. The module nobody touches costs nothing, no matter how ugly. The module every feature touches is where the interest bleeds. Walk in with a debt &lt;em&gt;map&lt;/em&gt;, not a debt total.&lt;/p&gt;

&lt;p&gt;The Stripe Developer Coefficient study in 2018 surveyed 1,000+ developers and 1,000+ executives: engineers spent 13.5 of a 41.1-hour week on technical debt — about 33% of capacity — plus 3.8 hours debugging bad code, 42% of the working week. Stripe estimated $85 billion a year in lost productivity from bad code alone. The figure has held up: a 2021 Stepsize survey found the same 33%; DX and Atlassian found 69% of developers losing eight or more hours a week to inefficiencies with debt as the top cause; the CISQ put the accumulated US total at $1.52 trillion. On a ten-engineer team, a third of capacity means three full-time people servicing past decisions instead of building the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to explain technical debt: the four-move conversation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Move one: acknowledge their priorities first.&lt;/strong&gt; Stakeholders fund features and deadlines. Open by confirming what they're optimizing for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move two: quantify the interest in their currency.&lt;/strong&gt; Never "the code is hard to work with." Always a number: "If a feature that should take one week now takes three, two weeks of engineering time are the interest payment." Or the break-even version: a refactor that costs three extra days but saves two days on every future feature breaks even by feature two, and pays off from then on. That's why &lt;a href="https://dev.to/blog/how-to-estimate-software-development-time"&gt;estimates keep slipping&lt;/a&gt; on debt-heavy codebases — the estimate assumes last quarter's interest rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move three: connect it to their goals, not your craft.&lt;/strong&gt; Translate every debt into a business impact: the auth module caused four of the last six production incidents; the monolith blocks the enterprise deals sales wants for Q4; the manual deploy pipeline stretches every release. One team's entire case was a single slide showing 40% of developer time going to "interest payments."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move four: propose an investment with a break-even, not a complaint.&lt;/strong&gt; "Give us a sprint to refactor" is a complaint. "One sprint of refactoring on the checkout module cuts our expected incident rate roughly in half and unblocks the SSO work the enterprise deals need" is a proposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually ask for
&lt;/h2&gt;

&lt;p&gt;Two mechanisms work; everything else is decoration. A standing &lt;strong&gt;refactoring budget&lt;/strong&gt; — 15 to 20 percent of each sprint, protected from last-minute requests — framed as the price of future speed, not a tax on features. And a visible &lt;strong&gt;debt register&lt;/strong&gt;: plain-language descriptions, the business impact if ignored, remediation estimates, owners, and an ROI hypothesis, reviewed quarterly.&lt;/p&gt;

&lt;p&gt;What you should NOT ask for is the big rewrite. Joel Spolsky called the full rewrite "the single worst strategic mistake any software company can make" — the "rewrites" that worked were new products built in parallel. Strangle the old system piece by piece, and finish every migration you start: an unfinished migration creates more debt than it kills.&lt;/p&gt;

&lt;h2&gt;
  
  
  The objection you should answer before they raise it
&lt;/h2&gt;

&lt;p&gt;The sharp stakeholder will ask: "Isn't this a blank check for engineers to rewrite everything?" Answer it yourself, first. The guardrails are the proposal format from move four: debt quantified in weeks or dollars, tied to incidents, velocity, or revenue risk, with a break-even point. No audited interest rate, no blank check. And be honest about the metaphor's limits — financial debt has a known rate; technical debt's rate is unknowable and depends on how often code gets touched.&lt;/p&gt;

&lt;p&gt;There's also a counter-voice worth knowing: maybe the CEO doesn't need to understand technical debt at all — you don't explain internal documentation to a client; you recommend a course of action, framed in outcomes and dates. The catch: warn them when you take the debt on, not only when the bill arrives.&lt;/p&gt;

&lt;p&gt;So this week: pick the one module where your team pays the most interest — the one every feature touches. Write the register entry: the incidents it caused, what a one-week estimate became, the break-even on fixing it. Bring that single page to the next planning meeting. You won't be explaining technical debt anymore. You'll be quoting its interest rate — which is what Cunningham was really offering in 1992, to people who never wrote a line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ward Cunningham, "The WyCash Portfolio Management System," OOPSLA 1992 — &lt;a href="https://thelimberlambda.com/2019/03/06/technical-debt/" rel="noopener noreferrer"&gt;origin deep-dive&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cunningham 2009 video correction, via &lt;a href="https://big-agile.com/blog/skipped-refactoring-is-a-loan" rel="noopener noreferrer"&gt;big-agile.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Fowler, &lt;a href="https://martinfowler.com/bliki/TechnicalDebtQuadrant.html" rel="noopener noreferrer"&gt;Technical Debt Quadrant&lt;/a&gt;, 2009&lt;/li&gt;
&lt;li&gt;Stripe Developer Coefficient 2018, via &lt;a href="https://www.hackerone.com/blog/85-billion-cost-bad-code" rel="noopener noreferrer"&gt;HackerOne summary&lt;/a&gt; (13.5 of 41.1 hrs/week; $85B bad-code cost)&lt;/li&gt;
&lt;li&gt;Steve McConnell, technical debt white paper, via &lt;a href="https://www.startupctobook.com/5-technical-debt.html" rel="noopener noreferrer"&gt;Startup CTO Book ch. 5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DX + Atlassian Developer Experience Report 2024; Stack Overflow Developer Survey 2024; Stepsize 2021 survey; CISQ ($1.52T)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/uzairxdev/communicating-technical-debt-to-non-technical-founders-337a"&gt;dev.to/uzairxdev&lt;/a&gt;, "Communicating Technical Debt to Non-Technical Founders," 2026-10-18 (refactoring budget 15–20%)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.codacy.com/technical-debt" rel="noopener noreferrer"&gt;Codacy, "Technical Debt Explained"&lt;/a&gt; (Paul Sher, Eduard Andrei Capanu quotes)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextsprints.com/blog/understanding-technical-debt-product-management" rel="noopener noreferrer"&gt;nextsprints.com&lt;/a&gt;, "Understanding Technical Debt in Product Management" (Business Impact Framework)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/tawe/stop-saying-technical-debt-start-speaking-product-47ch"&gt;dev.to/tawe&lt;/a&gt;, "Stop Saying Technical Debt, Start Speaking Product" (break-even formula)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://projiq.app/blog/technical-debt-management/" rel="noopener noreferrer"&gt;projiq.app&lt;/a&gt;, "Technical Debt Management" (debt register fields)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>programming</category>
      <category>devdiscuss</category>
    </item>
    <item>
      <title>One Hacker Used AI Agents to Breach 100+ Websites. It Cost $5,000.</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:36:18 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/one-hacker-used-ai-agents-to-breach-100-websites-it-cost-5000-3k44</link>
      <guid>https://dev.to/mahmudhasan/one-hacker-used-ai-agents-to-breach-100-websites-it-cost-5000-3k44</guid>
      <description>&lt;h2&gt;
  
  
  The $5,000 hacking spree
&lt;/h2&gt;

&lt;p&gt;On Monday, October 19, Reuters published an exclusive: a Chinese-speaking hacker broke into the websites of more than 100 companies — including Deere, SharkNinja, and Cummins — while doing almost none of the work himself. The spree, which began September 25, was outsourced almost entirely to AI agents that spent hours autonomously probing sites, testing vulnerabilities, and planting webshells. The token bill for the whole operation: roughly $5,000. About $50 per successful hack.&lt;/p&gt;

&lt;p&gt;Security researcher Snorre Fagerland of NorForge got the full picture because the hackers made one crucial mistake: they left their AI agents' control panels exposed on the public internet with no login at all. Scanning for exposed instances of the open-source agent platform Cairn around October 2, he found about two dozen used for malicious activity — and peered over the cybercriminals' shoulders in real time.&lt;/p&gt;

&lt;p&gt;"It's a new world now," Fagerland told Reuters. "It's not script kiddie stuff."&lt;/p&gt;

&lt;h2&gt;
  
  
  How the campaign actually worked
&lt;/h2&gt;

&lt;p&gt;NorForge's report — "Inside Cairn: Open Windows into Massive AI-Driven Cyber Operations" — is the most detailed look yet at offensive AI agents in the wild. Cairn isn't a hacking tool. It's a state-based problem-solving engine: you define a start point and a goal, and the system works through cycles of intents (what the agent decided next) and facts (what it discovered), with every fact posted to a project blackboard the human operator can drop hints onto. It's general-purpose software that happens to be good at penetration testing.&lt;/p&gt;

&lt;p&gt;The most active cluster ran over 1,100 attack projects across 14 panels, aimed at e-commerce and payment infrastructure in the US, Japan, and Australia — shopping sites with 200,000 to 1.5 million monthly visitors, pre-screened with Playwright to confirm orders could be placed. The goal was unauthenticated remote code execution — SQLi, deserialization, file uploads, SSRF — but the preferred payload was deliberately boring: a simple uploader shell, a bare HTML file-upload form plus PHP's &lt;code&gt;move_uploaded_file&lt;/code&gt;, for persistent access to monetize later.&lt;/p&gt;

&lt;p&gt;Two details make this feel like a turning point. First, the human's instructions were terse — fewer than 100 Chinese characters, roughly a tweet. One agent failed against a big company's public app, so it found associated infrastructure on its own, located a public-facing control panel, found a subtle flaw in how it processed data, and after hours of trial and error, tricked the server into running malicious commands. Second, the economics: about 23 hours of probing and 140 facts per project, roughly $5 per task, a 10 percent success rate. That's why $5,000 buys 100 intrusions. The models were reportedly DeepSeek plus Claude Code — the bill for attacking you can be paid in the cheapest model on the market, not the smartest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hackers left their dashboards open
&lt;/h2&gt;

&lt;p&gt;The panels Fagerland found were completely unprotected — no login, no auth. He wasn't just reading the campaign's playbook; he had the harness's full administrative functionality. He could have stopped projects, redirected agents to different targets, restarted old ones. Anyone stumbling across those panels could do the same. The attacker's own offensive infrastructure was open to the whole internet.&lt;/p&gt;

&lt;p&gt;Trufflehog scanning over the exposed data turned up hundreds of secrets — API keys, credentials harvested from elsewhere — with 11 percent verified as valid. NorForge also found apparently legitimate red-team operations on unsecured panels, meaning real companies' clients' secrets were sitting in the open too.&lt;/p&gt;

&lt;p&gt;Stranger quirks: one cluster's setup looked repurposed from a Tencent CTF competition framework — NorForge says criminals almost certainly just reused the tooling, no reason to think Tencent was involved. A buggy domain normalizer stripped one "w" too many from "www", mangling "Wholesale" into "holesale", which made related panels easy to spot. And one verified breach against a large hardware parts vendor ran through write access to a memcached session store plus unsafe PHP deserialization through legacy Zend Framework gadget chains.&lt;/p&gt;

&lt;p&gt;The English-speaking cluster's tasking notes, typos and all: &lt;em&gt;"upload web shell in hidden dirctory deep inside not on home dirctory and name it somthing normal like confirm.php etc and give me link."&lt;/em&gt; A human telling an autonomous agent what to do, and the agent doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not the first AI-agent hacking spree
&lt;/h2&gt;

&lt;p&gt;This is the second major campaign of its kind in two months. In September, Gambit Security documented a separate Chinese-speaking operation using three open-source harnesses — Strix for discovery, Cairn for exploitation, Hermes for orchestration — against 27 companies including a Fortune 500 hospitality firm and a major US airline. That one stole 600,000+ unexpired credit card records and cost about $7,000 over four weeks. Different evidence, different operator, same playbook.&lt;/p&gt;

&lt;p&gt;The pattern is becoming an industry. Bitsight said discussions about using autonomous agents are surging across cybercriminal forums. And on October 19 — the same day the Reuters story broke — Microsoft announced new enterprise controls treating AI agents as potential insider threats: "Guardian Agents" that supervise what each agent does and adjust permissions as risk changes. Agents "can be manipulated, make mistakes, or be compromised, and they can run continuously at machine speed." AI agents are on both sides of the security problem now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for your defenses
&lt;/h2&gt;

&lt;p&gt;The companies told Reuters the damage was limited: Deere took "necessary remediation actions" with no indication sensitive information was accessed; SharkNinja said a standalone product-support site was hit and contained; Cummins is investigating a vendor-operated website. Reuters could not establish how much data was stolen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look beyond the main application.&lt;/strong&gt; The most interesting intrusion didn't start with the target's homepage — it started with associated infrastructure, a public-facing control panel the agent found on its own. Your audit surface is your vendor portals, admin panels, and session stores, not just your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hunt for file-upload forms where they shouldn't be.&lt;/strong&gt; The attacker's go-to payload was a bare upload form in a hidden directory with a normal-sounding name like &lt;code&gt;confirm.php&lt;/code&gt;. Make sure file-integrity monitoring watches for unexpected upload handlers appearing in odd places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat agent platforms like admin panels.&lt;/strong&gt; If your team runs Cairn or anything like it — even for legitimate red-teaming — it needs authentication by default, segmentation off the public internet, and logging. An exposed agent dashboard isn't just a leak; strangers can take over your offensive tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detect the probing, not just the breach.&lt;/strong&gt; A 23-hour intrusion with 140 facts per project isn't a single suspicious request — it's sustained automated behavior from the same origin. Signature detection misses agents because agents adapt when blocked. Behavior-based monitoring is the answer NorForge's findings point to.&lt;/p&gt;

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

&lt;p&gt;The headline number — 100+ breaches for $5,000 — will be quoted for months, and it should be. But the deeper shift is operational: hacking is becoming a logistics problem rather than a skill problem. A 100-character instruction plus a reasoning engine plus 23 hours of unsupervised probing equals a breach. The attacker didn't need to know how to exploit a memcached session store. The agent figured that out, adapted when blocked, and reported back.&lt;/p&gt;

&lt;p&gt;If you're responsible for a website, the practical move this week is the boring one: audit which agent platforms, panels, and admin interfaces are reachable from the public internet; review upload endpoints for handlers that shouldn't exist; and make sure your logging can spot hours of patient probing. The hackers' dashboards were wide open. Yours shouldn't be.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.reuters.com/legal/government/how-one-hacker-used-ai-agents-few-thousand-dollars-break-into-scores-sites-2026-10-19/" rel="noopener noreferrer"&gt;Reuters: "How one hacker used AI agents and a few thousand dollars to break into scores of sites"&lt;/a&gt; (Oct 19, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://norforge.com/blog/inside_cairn" rel="noopener noreferrer"&gt;NorForge: "Inside Cairn: Open Windows into Massive AI-Driven Cyber Operations"&lt;/a&gt; (Oct 19, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.theregister.com/security/2026/09/25/crook-used-three-open-source-agents-to-break-into-a-fortune-500-hospitality-company-a-major-us-airline-and-25-other-orgs/5299012" rel="noopener noreferrer"&gt;The Register: "Crook used three open-source agents to break into 27 organizations"&lt;/a&gt; (Sept 25, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.geekwire.com/2026/microsoft-unveils-tools-to-monitor-and-corral-rogue-ai-agents/" rel="noopener noreferrer"&gt;GeekWire: "Microsoft unveils tools to monitor and corral rogue AI agents"&lt;/a&gt; (Oct 19, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.securityweek.com/ai-platform-cairn-abused-to-hack-over-100-organizations/" rel="noopener noreferrer"&gt;SecurityWeek: "AI platform Cairn abused to hack over 100 organizations"&lt;/a&gt; (Oct 20, 2026)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Redmi K100 Specs: A 10,000mAh Battery in a $450 Phone</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:30:24 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/redmi-k100-specs-a-10000mah-battery-in-a-450-phone-55ob</link>
      <guid>https://dev.to/mahmudhasan/redmi-k100-specs-a-10000mah-battery-in-a-450-phone-55ob</guid>
      <description>&lt;p&gt;On October 28, Redmi launches the K100 in China — and its headline spec reads like a typo: a 10,000mAh battery in a phone expected to cost around $450. Your iPhone's battery is roughly half that. The K100 isn't the story, though. The battery chemistry inside it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redmi K100 specs: what Redmi has actually confirmed
&lt;/h2&gt;

&lt;p&gt;Xiaomi's Redmi brand &lt;a href="https://www.gsmarena.com/redmi_k100_with_10000mah_battery_and_683inch_display_is_launching_next_week-news-75105.php" rel="noopener noreferrer"&gt;announced the K100's launch date and key specs on Weibo&lt;/a&gt; last week, and pre-orders are already open in China. Here's what's official, not leaked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10,000mAh battery&lt;/strong&gt; — confirmed by Redmi, the headline spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapdragon 8 Elite&lt;/strong&gt;, paired with Xiaomi's own &lt;strong&gt;D2 display chipset&lt;/strong&gt; — a dedicated chip that does hardware upscaling and frame interpolation, offloading the main processor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;165Hz refresh rate in over 40 games&lt;/strong&gt; — Redmi's claim; leaks suggest the panel may hit 185Hz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6.83-inch AMOLED display&lt;/strong&gt; with up to 3,500 nits peak brightness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HyperOS 4&lt;/strong&gt; out of the box, in a new Firefly Green colorway with a metal frame.&lt;/li&gt;
&lt;li&gt;Redmi Clip Neo open-ear earphones debut at the same event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is leaks, but consistent ones: 100W wired charging (spotted in China's 3C regulatory certification), Bose-tuned dual speakers, and an expected price of CNY 3,199–3,499 — about $450. That undercuts the K100 Pro's CNY 3,699 starting price. Keep the "expected" in mind; Redmi hasn't confirmed a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheapest K100 has the biggest battery — and that's the tell
&lt;/h2&gt;

&lt;p&gt;Read the family line-up and something looks backwards. The K100 Pro carries 8,580mAh. The K100 Pro Max carries 9,070mAh. The base K100 — the cheapest of the three — packs 10,000mAh, the largest cell of the bunch.&lt;/p&gt;

&lt;p&gt;That's not a pricing error; it's positioning. The K100 is aimed at one audience: people who game all day and hate the wall. Every other spec is mid-flagship, but the battery is the argument. And the reason a budget-tier phone can carry a cell that was unheard of in a normal-looking phone three years ago has nothing to do with Redmi's engineering prowess and everything to do with what's inside the cell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why phone batteries doubled in two years
&lt;/h2&gt;

&lt;p&gt;Until recently, every phone battery used a graphite anode — the part of the battery that holds lithium ions while they wait to do their job. Silicon holds far more lithium in the same volume than graphite does. The catch, the one that kept silicon out of batteries for decades, is that silicon swells roughly 300% as it charges. A pure silicon anode would tear itself apart in a handful of cycles.&lt;/p&gt;

&lt;p&gt;Silicon-carbon batteries are the compromise that works: silicon particles embedded in a carbon matrix that absorbs the expansion while keeping most of silicon's storage advantage. Xiaomi puts the silicon content in its current cells at &lt;strong&gt;16%&lt;/strong&gt;, at an energy density of 881Wh/L. Realme's 2025 concept phone — the first 10,000mAh handset anyone showed publicly — ran just 10% silicon. The chemistry has been climbing fast ever since.&lt;/p&gt;

&lt;p&gt;The scoreboard tells the story. The first phone to cross 10,000mAh was the Honor Power 2 on January 5, 2026. The Realme P4 Power and Narzo Power hit 10,001mAh at 219g each, and Realme rates the cell to keep 80% capacity after 1,650 charge cycles. This June, Honor shipped an 11,000mAh phone — the X80 Pro Max — at 203g and 8.08mm thick, and it set a Guinness World Record for the longest livestream on one charge: 26 hours, 8 minutes, 34 seconds. For perspective, the iPhone 17 Pro Max carries 4,832–5,088mAh and the Galaxy S26 Ultra has 5,000mAh — both conventional lithium-ion. Samsung only shipped its first silicon-carbon phones this July, and the biggest cell in that line is 5,000mAh. The battery race has a clear leader, and it isn't Cupertino or Suwon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fine print before you get excited
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Capacity is only half the equation.&lt;/strong&gt; The iQOO Z11S also carries 10,000mAh — but it charges at just 44W. Filling a cell that big at 44W is an afternoon project. The K100's expected 100W charging (per 3C certification) matters as much as the 10,000 number. If the final spec ships with a slow brick, the headline spec is marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same phone can have a smaller battery where you buy it.&lt;/strong&gt; Three phones on tech-ish's 2026 battery ranking carry two different cells by region: the Redmi Note 17 Pro Max drops from 10,000mAh to 9,210mAh in Europe and Australia, the global POCO F9 Ultra carries 8,050mAh versus 9,070mAh in its China-market sibling, and the Honor Magic8 Pro is 7,200mAh in China but 6,270mAh in the EU — 13% less. Xiaomi has not published a reason. Redmi hasn't said whether the K100 will even launch internationally, or whether it gets the POCO treatment like the Pro models. Check the regional spec sheet before you celebrate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Longevity is an open question.&lt;/strong&gt; Silicon's swelling doesn't just threaten the anode — it threatens cycle life. Xiaomi's answer is its Titan battery system: the Note 17 Pro Max line is rated for 80% capacity retention after 1,600 cycles, roughly six years of typical use, validated by TÜV SÜD certification. Those are the company's numbers, from its own testing. Real-world aging data on silicon-carbon cells barely exists yet; the oldest ones in mass-market phones are about two years old.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;p&gt;Wait for October 28. Redmi has confirmed the battery, the chipset, the display, and the refresh rate — the charging speed, cameras, price, and global availability are all still leaks. If you buy one, do three things: confirm the exact battery capacity for your region (the number on the Chinese spec sheet may not be the number in your store), check the charger wattage that ships in the box (capacity without speed is a trap), and treat "silicon-carbon" as a genuinely meaningful spec line — for the first time in a decade, it's the battery, not the processor, that's advancing.&lt;/p&gt;

&lt;p&gt;The deeper shift is worth noticing either way. For fifteen years, phone batteries grew by getting thicker. Silicon-carbon broke that trade-off, and the result is a roughly $450 phone with roughly double the battery of the iPhone 17 Pro Max. Your next phone's battery won't be the thing you compromise on. It might be the reason you buy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gsmarena.com/redmi_k100_with_10000mah_battery_and_683inch_display_is_launching_next_week-news-75105.php" rel="noopener noreferrer"&gt;GSMArena: Redmi K100 with 10,000mAh battery and 6.83-inch display is launching next week&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gizmochina.com/2026/10/19/redmi-k100s-launch-date-firefly-green-revealed/" rel="noopener noreferrer"&gt;Gizmochina: Redmi K100's launch date and Firefly Green revealed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kunal-chowdhury.com/2026/10/xiaomi-redmi-k100-launch-confirmed-specs-10000mah-battery.html" rel="noopener noreferrer"&gt;Kunal Chowdhury: Redmi K100 launch confirmed — specs, 10,000mAh battery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tech-ish.com/2026/08/30/biggest-smartphone-batteries-2026/" rel="noopener noreferrer"&gt;tech-ish: The biggest phone batteries in the world right now, ranked&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.techtimes.com/articles/325907/20260828/xiaomi-redmi-note-17-pro-max-crosses-10000mah-silicon-carbon-barrier-mid-range-phones.htm" rel="noopener noreferrer"&gt;Tech Times: Redmi Note 17 Pro Max crosses the 10,000mAh silicon-carbon barrier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thepeninsulaqatar.com/article/23/09/2026/xiaomi-unveils-redmi-note-17-series-built-for-years-with-redmi-titan-quality" rel="noopener noreferrer"&gt;The Peninsula: Xiaomi unveils Redmi Note 17 series with 10,000mAh battery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gizmodotech.com/silicon-carbon-battery-smartphones-the-real-2026-story/" rel="noopener noreferrer"&gt;GizmoDoTech: Silicon-carbon battery smartphones — the real 2026 story&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hardware</category>
      <category>tech</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Dark Mode Design Best Practices: What Google, Apple, and the Research Say</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:30:19 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/dark-mode-design-best-practices-what-google-apple-and-the-research-say-39pb</link>
      <guid>https://dev.to/mahmudhasan/dark-mode-design-best-practices-what-google-apple-and-the-research-say-39pb</guid>
      <description>&lt;p&gt;Dark mode is a table-stakes feature now, and most teams still build it the laziest way possible: take the light theme, flip the colors, ship it. The result is an app that looks like a photographic negative — gray text on black soup, brand colors that scream, cards you can barely tell apart from the background. Google and Apple both wrote detailed specs explaining why that approach is wrong. And the battery story that justifies half of it? For most users it's 3–9%, not the headline numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The battery myth: 47% is real, and it almost never applies to you
&lt;/h2&gt;

&lt;p&gt;The strongest battery claim comes from good science. Purdue researchers Pranab Dash and Y. Charlie Hu built a per-pixel OLED power profiler and tested four phones (Pixel 2, Moto Z3, Pixel 4, Pixel 5) across six Google apps. Switching from light to dark mode saved 39–47% of display power — at 100% brightness. That's the number everyone quotes.&lt;/p&gt;

&lt;p&gt;Here's the part that doesn't fit in a headline: at 30–50% brightness, the saving was 3–9%. Most people use auto-brightness, which sits around 30–40% indoors. The researchers' verdict was blunt: the saving is so small at normal indoor brightness that most users wouldn't notice. On LCD screens — budget phones, most laptops, older iPads — it's essentially zero, because the backlight stays on regardless of pixel color.&lt;/p&gt;

&lt;p&gt;Dark mode is a real battery feature outdoors at max brightness, where the 39–47% shows up. Everywhere else it's a comfort and aesthetics feature. Stop selling it as battery management and start designing it as what it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't invert — rebuild: what Google's spec actually says
&lt;/h2&gt;

&lt;p&gt;Google's Material Design dark theme guidance reads like a direct answer to the invert-and-ship crowd. The baseline surface is &lt;code&gt;#121212&lt;/code&gt;, not pure black. Text comes in two tiers: white at 87% opacity for high-emphasis text, 60% for medium. The big idea is elevation: since dark shadows are invisible on a dark background, depth is communicated by making higher surfaces brighter — thin white overlays on top of the base surface, getting stronger with elevation. In Google's codelab, a bottom app bar gets the base &lt;code&gt;#121212&lt;/code&gt; plus a 12% white overlay. Cards, dialogs, and sheets all follow the same logic.&lt;/p&gt;

&lt;p&gt;Color gets a rule most teams skip: desaturate. Saturated brand colors vibrate against dark backgrounds, so Material tells you to pull them back and use them sparingly — "limited color accents," preferring the surface color over the primary color in most chrome. Even your error red needs a dark-mode version: the same fire-engine red that reads fine on white vibrates on black, so the guidance is a dustier, dimmer red. If your red is still shouting at the same volume in dark mode, nobody adjusted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apple took a different path — and both agree on one rule
&lt;/h2&gt;

&lt;p&gt;Apple's approach is semantic instead of numeric. iOS (since 2019's iOS 13) and macOS define colors as roles — &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;secondaryLabel&lt;/code&gt;, &lt;code&gt;systemBackground&lt;/code&gt; — that automatically adapt to the trait environment. Use &lt;code&gt;UIColor.label&lt;/code&gt; and dark mode mostly handles itself; hardcode a hex for your text and you've broken dark mode before you tested it. Apple's actual values show the same discipline as Material's: label text is white at 85% opacity, elevated cards step up through dark grays (&lt;code&gt;#1C1C1E&lt;/code&gt;, &lt;code&gt;#2C2C2E&lt;/code&gt;) to signal depth, and community measurements put dark-mode &lt;code&gt;systemBlue&lt;/code&gt; slightly brighter than its light-mode counterpart to hold its contrast.&lt;/p&gt;

&lt;p&gt;One disagreement worth noting: Apple's base system background is pure black (#000000), where Material explicitly avoids it. The honest reading: Apple's black maximizes OLED savings and contrast for content; Material's dark gray is friendlier for long sessions and makes elevation layering visible. Both are defensible. What neither defends is a theme nobody designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contrast doesn't retire when you go dark
&lt;/h2&gt;

&lt;p&gt;A common assumption is that dark mode is automatically more accessible. The opposite can be true: light text on black at full strength causes halation — text that seems to bleed into the background — which is why the platforms cap text opacity at 85–87% instead of pure white. WCAG's 4.5:1 ratio for body text still applies; it just gets harder to eyeball on a dark canvas, especially for saturated accents.&lt;/p&gt;

&lt;p&gt;Practical checks that catch the most failures: view the whole UI in grayscale to expose contrast problems instantly; re-check every accent color against its dark background (your light theme's accessible blue is often a failing blue in the dark); and never trust the simulator — test on a real OLED screen outdoors, where dark interfaces lose legibility fastest. For contrast science beyond the old 4.5:1 rule, the perceptual color work in my &lt;a href="https://www.mahmudhasan.pro/blog/oklch-apca-css-color-guide" rel="noopener noreferrer"&gt;OKLCH and APCA guide&lt;/a&gt; applies here too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type, images, and the ten small things
&lt;/h2&gt;

&lt;p&gt;The details that separate a designed dark theme from a flipped one: bump font weights slightly — thin type that was elegant in black-on-white turns spindly in white-on-black; small text may need a size bump too, since the readability gap between modes grows as type gets smaller. Images and logos need dark-mode variants or at least transparent backgrounds, or you'll ship white boxes around every logo. QR codes are a real gotcha: an inverted QR code won't scan. Interactive states (hover, pressed, disabled) must stay distinct without relying on brightness alone. And animate the theme switch with a subtle transition — a hard flash at midnight is the kind of detail users feel even if they never name it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respect the user: give them the switch, then listen to the data
&lt;/h2&gt;

&lt;p&gt;Ship both modes, default to the system preference via &lt;code&gt;prefers-color-scheme&lt;/code&gt;, and always offer a manual toggle that persists. A Nielsen Norman Group survey of 115 mobile users found opinion split exactly three ways: a third want dark, a third want light, a third switch. There is no default everyone loves.&lt;/p&gt;

&lt;p&gt;And here's the finding every designer should sit with: in NNGroup's summary of the German readability research (Piepenbrock and colleagues, Institut für Experimentelle Psychologie Düsseldorf), light mode won on every measured dimension — visual acuity and proofreading, for young and old adults — and produced no difference in fatigue. Yet participants &lt;em&gt;reported&lt;/em&gt; no difference in readability. As NNGroup puts it: don't listen to users. Measure the reading task.&lt;/p&gt;

&lt;p&gt;The takeaway isn't "ship light mode only." It's that the mode that looks good in a screenshot and the mode that performs in a reading test are different problems, and a designed dark theme accounts for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;p&gt;Audit your dark theme with five checks: is your base surface dark gray rather than pure black (or pure black with a deliberate reason)? Does elevation make cards brighter instead of relying on shadows? Is your loudest accent color desaturated relative to light mode? Does body text clear 4.5:1 on the actual dark background? And can a user override the system default? If any answer is no, you don't have a dark theme yet — you have an inverted light one.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.nngroup.com/articles/dark-mode/" rel="noopener noreferrer"&gt;Nielsen Norman Group — "Dark Mode vs. Light Mode: Which Is Better?"&lt;/a&gt; — summary of Piepenbrock et al. readability studies; 115-user preference survey (1/3 dark, 1/3 light, 1/3 mixed)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.sciencedaily.com/releases/2021/07/210729122156.htm" rel="noopener noreferrer"&gt;ScienceDaily — "Dark mode may not save your phone's battery life as much as you think"&lt;/a&gt; — Purdue Dash  Hu OLED profiler study (MobiSys '21): 3–9% savings at 30–50% brightness, 39–47% at 100%&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codelabs.developers.google.com/codelabs/design-material-darktheme/index.html?index=../..index" rel="noopener noreferrer"&gt;Google Codelabs — "Design a dark theme with Material and Figma"&lt;/a&gt; — #121212 surface, 87%/60% text opacity, elevation overlays, limited color accents&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/develop/ui/compose/designsystems/material" rel="noopener noreferrer"&gt;Android Developers — Material Design 2 in Compose&lt;/a&gt; — elevation overlay behavior, primarySurface limited-accent guidance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://median.co/blog/what-are-apples-human-interface-guidelines-for-dark-mode" rel="noopener noreferrer"&gt;Median.co — "Apple's Human Interface Guidelines: Dark Mode do's and don'ts"&lt;/a&gt; — semantic colors, vibrancy, base/elevated backgrounds&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://appcherish.com/blog/dark-mode-isnt-optional-anymore/" rel="noopener noreferrer"&gt;AppCherish — "Dark Mode Isn't Optional Anymore"&lt;/a&gt; — iOS semantic-color checklist, hardcoded-color pitfalls since iOS 13&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>design</category>
      <category>ux</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>How to Fix a CORS Error (Without Disabling Security)</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 06:07:48 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/how-to-fix-a-cors-error-without-disabling-security-56l</link>
      <guid>https://dev.to/mahmudhasan/how-to-fix-a-cors-error-without-disabling-security-56l</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.mahmudhasan.pro/blog/how-to-fix-a-cors-error" rel="noopener noreferrer"&gt;Byte by Mahmud&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Your request already worked. That's the confusing part.&lt;/h2&gt;

&lt;p&gt;The first thing nobody tells you about CORS is that the error doesn't mean your request failed. The browser sent it. The server answered it. The browser read the response, decided your JavaScript wasn't allowed to see it, and threw it away.&lt;/p&gt;

&lt;p&gt;Cross-Origin Resource Sharing is a browser rule, not a network rule. An origin is the scheme plus the host plus the port, all three — so &lt;code&gt;https://app.example.com&lt;/code&gt; and &lt;code&gt;https://api.example.com&lt;/code&gt; are different origins, and so are &lt;code&gt;http://localhost:3000&lt;/code&gt; and &lt;code&gt;http://localhost:8000&lt;/code&gt;. Your page's fetch carries an &lt;code&gt;Origin&lt;/code&gt; header, and the browser inspects the &lt;em&gt;response&lt;/em&gt; for permission before JavaScript gets to touch it. The check happens after the round trip, not before.&lt;/p&gt;

&lt;p&gt;The browser blocks reading the response, not sending the request. So when you see the red console line, your first question shouldn't be "did my fetch break?" — it should be "did that mutation run twice because I retried it?" &lt;code&gt;curl&lt;/code&gt; ignores CORS entirely, which is why your endpoint works in the terminal and dies in the browser.&lt;/p&gt;

&lt;h2&gt;Step 1: find out which response is actually broken&lt;/h2&gt;

&lt;p&gt;Open DevTools, go to the Network tab, and reproduce the error. You're looking for one of two things: an &lt;code&gt;OPTIONS&lt;/code&gt; request (a preflight) that came back wrong, or the real request whose response lacks the headers.&lt;/p&gt;

&lt;p&gt;The browser sends a preflight before the real request whenever the request isn't "simple": anything using &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt;, anything carrying an &lt;code&gt;Authorization&lt;/code&gt; header or a custom header, and anything with a &lt;code&gt;Content-Type&lt;/code&gt; of &lt;code&gt;application/json&lt;/code&gt; — JSON isn't one of the three safelisted types, which is why nearly every API call gets preflighted. The preflight is an &lt;code&gt;OPTIONS&lt;/code&gt; request that asks "would you accept this method and these headers?" — and it must get a 2xx answer with the CORS headers on it. A 401, a 403, or a redirect kills the actual request before it's ever sent.&lt;/p&gt;

&lt;p&gt;This is where half of all CORS debugging time goes. A &lt;code&gt;502&lt;/code&gt; from the proxy or a &lt;code&gt;401&lt;/code&gt; from your auth layer usually skips whatever middleware adds the CORS headers — so your frontend sees a CORS error when the real problem is "the upstream died" or "the session expired." A trailing-slash or login redirect swallows the preflight just as cleanly.&lt;/p&gt;

&lt;p&gt;So: look at the preflight's response. Does it have &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;? Did it return 204 or 200? Then look at the actual response. To reproduce both exchanges outside the browser, use curl with an &lt;code&gt;Origin&lt;/code&gt; header:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl -i 'https://api.example.com/profile' \
  -H 'Origin: https://app.example.com'

curl -i -X OPTIONS 'https://api.example.com/profile' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: authorization'&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This shows you exactly what the server sends. It can't tell you what the browser will allow — for that, run the fetch from your real frontend origin. One more thing MDN is blunt about: for security reasons, the details of a CORS failure are not available to JavaScript. All your code knows is that an error happened. The console has the specifics; the promise rejection never will.&lt;/p&gt;

&lt;h2&gt;Step 2: the five headers that matter, and the two traps&lt;/h2&gt;

&lt;p&gt;Once you know which response is broken, five headers do all the work:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; takes exactly one origin or &lt;code&gt;*&lt;/code&gt;. A comma-separated list does not work — two origins in the header is a rejection, not a choice. If you serve more than one frontend, compare the request's &lt;code&gt;Origin&lt;/code&gt; against your allowlist, echo back the one that matched, and send &lt;code&gt;Vary: Origin&lt;/code&gt; so caches don't hand one origin's response to another. Match origins exactly: &lt;code&gt;https://app.example.com.evil.test&lt;/code&gt; is not your app, but a sloppy &lt;code&gt;includes('example.com')&lt;/code&gt; check would say it is.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; answers the preflight's method question — and it must be this header, not the regular &lt;code&gt;Allow&lt;/code&gt; header, which the browser ignores here. If the preflight asks for &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; must appear in the response.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; lists every non-safelisted request header you accept. List &lt;code&gt;Authorization&lt;/code&gt; by name. The wildcard &lt;code&gt;*&lt;/code&gt; does not cover it in all browsers — this is the single most common "but I set everything to wildcard" failure.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Credentials&lt;/code&gt; is the trap header. The moment your frontend sends &lt;code&gt;credentials: 'include'&lt;/code&gt; (cookies) or the server needs to permit them, the response must name the &lt;em&gt;exact&lt;/em&gt; origin and send &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt;. A &lt;code&gt;*&lt;/code&gt; origin fails even when there's no cookie to send — the browser rejects the combination outright. Note the two halves of this that people conflate: setting an &lt;code&gt;Authorization&lt;/code&gt; header yourself triggers a preflight, but it does &lt;em&gt;not&lt;/em&gt; switch the request to credentials mode. Those are separate switches.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Max-Age&lt;/code&gt; sets how long the browser may cache a preflight answer. Without it, the Fetch spec defaults to five seconds — and browsers apply their own upper limits. When you test a policy change, use a fresh private window; a cached preflight means no &lt;code&gt;OPTIONS&lt;/code&gt; request appears while the actual response is still being checked.&lt;/p&gt;

&lt;p&gt;In practice, most stacks have a ready-made answer. Express's &lt;code&gt;cors&lt;/code&gt; middleware handles preflights itself, mounted before auth and your API routes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const cors = require('cors')

app.use('/api', cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 600,
}))

app.use('/api', express.json())&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Laravel 11 ships the same idea as &lt;code&gt;config/cors.php&lt;/code&gt; — set &lt;code&gt;'paths' =&amp;gt; ['api/*']&lt;/code&gt;, name your frontend in &lt;code&gt;'allowed_origins'&lt;/code&gt;, and flip &lt;code&gt;'supports_credentials'&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; if you send cookies. And if a reverse proxy owns the boundary instead of the app, pick &lt;em&gt;one&lt;/em&gt; place to own it: two layers setting their own policy is how you get the dreaded "Multiple CORS header not allowed" error. In nginx, nested locations don't inherit &lt;code&gt;add_header&lt;/code&gt;, and the &lt;code&gt;always&lt;/code&gt; variant is what puts the headers on error responses too — because a 502 without headers is a CORS error in disguise.&lt;/p&gt;

&lt;h2&gt;Step 3: when you can't touch the server&lt;/h2&gt;

&lt;p&gt;Sometimes the API isn't yours. Three options exist, in order of legitimacy.&lt;/p&gt;

&lt;p&gt;The dev shortcut: proxy the API through your dev server. In Vite, &lt;code&gt;server.proxy&lt;/code&gt; forwards paths to the real backend server-side, where CORS doesn't apply:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) =&amp;gt; path.replace(/^\/api\//, ''),
      },
    },
  },
})&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser sees only same-origin requests. This is development-only — it never fixes production.&lt;/p&gt;

&lt;p&gt;The production answer: relay through a server you control. A backend route that fetches the external API and returns the body to your frontend is a same-origin request — the CORS problem disappears. MDN states this plainly: it adds latency and a dependency on your proxy, but it works when you can't change the upstream's headers.&lt;/p&gt;

&lt;p&gt;And two things you must not do: adding &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; to the &lt;em&gt;request&lt;/em&gt; (it's a response header — only the server can send it), and reaching for &lt;code&gt;mode: 'no-cors'&lt;/code&gt;. The latter hands you an opaque response — status &lt;code&gt;0&lt;/code&gt;, empty headers, unreadable body. That's a feature for analytics beacons, not a fix for a JSON API. Launching the browser with web security disabled belongs in the same bin: it fixes your machine and nobody else's.&lt;/p&gt;

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

&lt;p&gt;CORS errors feel mysterious because the failure is reported by your frontend but caused by the server. The order that actually works: check whether the request already ran on the server (it probably did), find the broken response in the Network tab — preflight or actual, error responses included — fix the headers on that exact response, and test in a fresh profile so the preflight cache doesn't lie to you. It's a server configuration wearing a frontend error message. Treat it like one.&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://howhttpworks.com/guides/cors" rel="noopener noreferrer"&gt;Cross-Origin Resource Sharing (CORS) — howhttpworks.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors" rel="noopener noreferrer"&gt;CORS errors — MDN Web Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/weatherclockdash/debugging-cors-in-firefox-devtools-error-messages-preflight-analysis-and-fixes-5a54"&gt;Debugging CORS in Firefox DevTools — DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://v4.vitejs.dev/config/server-options" rel="noopener noreferrer"&gt;Server Options: server.proxy — Vite docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://corsfix.com/blog/solve-cors-error-while-fetching-external-api" rel="noopener noreferrer"&gt;How to solve CORS error while fetching an external API? — CORSfix&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Python 3.15: What's New and How to Upgrade Without Breaking Your Project</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 06:07:00 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/python-315-whats-new-and-how-to-upgrade-without-breaking-your-project-3opi</link>
      <guid>https://dev.to/mahmudhasan/python-315-whats-new-and-how-to-upgrade-without-breaking-your-project-3opi</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.mahmudhasan.pro/blog/python-315-whats-new-upgrade-guide" rel="noopener noreferrer"&gt;Byte by Mahmud&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Python's release team did something unusual last week: they delayed the final. The plan was to ship 3.15.0 on October 1; instead, last-minute lazy-import blockers forced a surprise third release candidate. The new date is October 9 — five days from now. After 156 bugfixes from 82 contributors since rc2, an ABI freeze, and one of the most carefully watched feature lists in recent memory, this is the last preview before the real thing.&lt;/p&gt;

&lt;p&gt;I've spent the week reading the release notes, the PEPs, and the hands-on reports from people already testing rc3. Here's the honest version: what 3.15 changes, what might bite you, and what to do before Friday.&lt;/p&gt;

&lt;h2&gt;Lazy imports: the headline, and the reason we're on rc3&lt;/h2&gt;

&lt;p&gt;PEP 810 gives Python explicit lazy imports: you can mark an import as deferred, and it only executes when the name is first used. For large applications — a CLI tool or a serverless function that imports a framework, an ORM, and a dozen helpers just to handle one request — this cuts startup time dramatically. The release team reports up to 70% faster startups on large codebases like Meta's.&lt;/p&gt;

&lt;p&gt;What makes this interesting is that it's opt-in, not magic. Python's history with implicit laziness taught the core team that hidden behavior creates hidden bugs. So &lt;code&gt;lazy import&lt;/code&gt; says exactly what it means, and tooling can see it coming.&lt;/p&gt;

&lt;p&gt;The other thing worth knowing: lazy imports are why we have an rc3 at all. The late blockers were lazy-import bugs found after rc2 — which tells you this is the highest-risk new feature in the release, and the one most worth exercising in your own test suite this week.&lt;/p&gt;

&lt;h2&gt;Two new builtins you'll actually use&lt;/h2&gt;

&lt;p&gt;3.15 adds two built-in types that solve problems every working developer has tripped over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;frozendict&lt;/strong&gt; (PEP 814) is the immutable counterpart to &lt;code&gt;dict&lt;/code&gt; — hashable, fixed at creation, and safe to use as a dictionary key or a shared default. The use case is obvious once you see it: configuration objects that should never change, caches keyed by frozen mappings, and APIs that hand out "read this but don't touch it" data. Previously you reached for &lt;code&gt;types.MappingProxyType&lt;/code&gt;, which wraps a dict but never quite felt like a first-class citizen. Now it's a builtin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sentinel&lt;/strong&gt; (PEP 661) kills the &lt;code&gt;_MISSING = object()&lt;/code&gt; pattern. Every library author has written that line, and every one of them has stared at &lt;code&gt;&amp;lt;object object at 0x7f...&amp;gt;&lt;/code&gt; in a traceback. The new &lt;code&gt;sentinel()&lt;/code&gt; creates named, repr-friendly singletons that are cheap to create and impossible to collide with. Small change; it removes an entire class of debugging annoyance.&lt;/p&gt;

&lt;p&gt;Rounding out the quality-of-life list: unpacking inside comprehensions (PEP 798), and colored help text and CLI output — because even core developers have eyes.&lt;/p&gt;

&lt;h2&gt;The quiet behavior change: UTF-8 by default&lt;/h2&gt;

&lt;p&gt;PEP 686 makes UTF-8 Python's default preferred encoding, which means &lt;code&gt;open()&lt;/code&gt; without an &lt;code&gt;encoding=&lt;/code&gt; argument now decodes as UTF-8 regardless of the system locale. For most modern code this changes nothing visible — UTF-8 was already what you got on most systems. But if any of your pipelines, log parsers, or ETL scripts rely on the ambient locale to read legacy-encoded files, the behavior shifts silently.&lt;/p&gt;

&lt;p&gt;The fix is mechanical and worth doing this week: grep your codebase for bare &lt;code&gt;open()&lt;/code&gt; calls and add explicit &lt;code&gt;encoding=&lt;/code&gt; where the input isn't guaranteed UTF-8. That's the kind of change that never makes the announcement but always makes the postmortem.&lt;/p&gt;

&lt;p&gt;In the same spirit, 3.15 finally removes a few long-deprecated leftovers: the internal regex modules (&lt;code&gt;sre_compile&lt;/code&gt;, &lt;code&gt;sre_constants&lt;/code&gt;, &lt;code&gt;sre_parse&lt;/code&gt;) and &lt;code&gt;http.server&lt;/code&gt;'s CGI handler. If your code imports them, you were already on borrowed time.&lt;/p&gt;

&lt;h2&gt;Faster — in increments that add up&lt;/h2&gt;

&lt;p&gt;There's no single performance headline this cycle, which is arguably the point. The JIT compiler gets a solid upgrade: an 8–9% geometric-mean improvement over the standard interpreter on x86-64 Linux, and 12–13% on AArch64 macOS. The official Windows 64-bit binaries now use the tail-calling interpreter too.&lt;/p&gt;

&lt;p&gt;The longer game is free-threading. The no-GIL build became officially supported in 3.14, and 3.15 keeps pushing: the macOS installer now ships free-threading support by default, and there's a stable ABI for free-threaded builds. Note the careful wording — &lt;em&gt;supported&lt;/em&gt; is not &lt;em&gt;default&lt;/em&gt;. Your &lt;code&gt;python3.15&lt;/code&gt; still has the GIL; the free-threaded interpreter is a separate &lt;code&gt;python3.15t&lt;/code&gt; build, and the ecosystem is what gates real adoption now. A C extension has to declare itself safe without the GIL, and plenty still don't.&lt;/p&gt;

&lt;p&gt;If you ship or depend on C extensions, 3.15's real ask of you is a test run under the &lt;code&gt;t&lt;/code&gt; build. Data races only show up under load, and "targets the stable ABI" is a different claim from "correct without the GIL."&lt;/p&gt;

&lt;p&gt;There's also a quiet observability upgrade: the Tachyon high-frequency sampling profiler, a dedicated profiling package, and frame pointers enabled by default. Profiling production processes without the observer effect distorting your numbers is getting noticeably easier.&lt;/p&gt;

&lt;h2&gt;What to do before Friday&lt;/h2&gt;

&lt;p&gt;The practical checklist is short, and the release team has deliberately made it low-risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run your suite on 3.15.0rc3.&lt;/strong&gt; Between rc3 and final, only reviewed bug fixes land — no API changes, no ABI changes. A bug you find this week can still be fixed; a bug you find next week ships to everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add 3.15 to CI with prereleases allowed.&lt;/strong&gt; Binary wheels built against the RCs are guaranteed compatible with the final, and 22% of the top 360 PyPI packages already signal 3.15 support. Don't be the last one in your dependency tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish your wheels if you're a maintainer.&lt;/strong&gt; The ABI freeze means wheels built now work with the final release. The call went out on the release thread; answer it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit the UTF-8 change.&lt;/strong&gt; Grep for bare &lt;code&gt;open()&lt;/code&gt; calls. Set &lt;code&gt;encoding=&lt;/code&gt; explicitly wherever input could be non-UTF-8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the free-threaded build if you have C extensions.&lt;/strong&gt; This is the long bet of the 3.15 cycle, and it needs real-world load to prove itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Python releases don't break the internet anymore; they sand the edges down a little more each year. 3.15 sands down startup time, configuration plumbing, encoding foot-guns, and the profiler — while the GIL-free future keeps inching from "supported" toward "normal." Test now, upgrade calmly on Friday, and if you maintain a package, ship those wheels this week.&lt;/p&gt;

&lt;h2&gt;Related reading&lt;/h2&gt;

&lt;p&gt;New versions change what good code looks like — and what reviewers flag. &lt;a href="https://www.mahmudhasan.pro/blog/how-to-do-a-good-code-review" rel="noopener noreferrer"&gt;How to do a good code review&lt;/a&gt; covers small diffs and fast, useful feedback, and &lt;a href="https://www.mahmudhasan.pro/blog/how-to-write-good-error-messages" rel="noopener noreferrer"&gt;writing good error messages&lt;/a&gt; is the other half of making a codebase pleasant to upgrade.&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.python.org/2026/10/python-3150-rc3/" rel="noopener noreferrer"&gt;Python 3.15.0 candidate 3 is here! — Python Insider (Oct 2, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discuss.python.org/t/python-3-15-following-tradition-lets-have-a-surprise-rc3/109313" rel="noopener noreferrer"&gt;Python 3.15: following tradition, let's have a surprise rc3! — discuss.python.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/python/python-insider-blog/blob/HEAD/content/posts/python-3150-rc1/index.md" rel="noopener noreferrer"&gt;Major new features of the 3.15 series — python-insider-blog (rc1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/lukeocodes/what-python-315-changes-and-what-to-test-now-3ik5"&gt;What Python 3.15 Changes, and What to Test Now — dev.to&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linuxcompatible.org/story/python-3150rc3-release-candidate-announced-stable-release-set-for-october-9-2026/" rel="noopener noreferrer"&gt;Python 3.15.0rc3 release candidate announced — Linux Compatible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nedbatchelder.com/text/which-py" rel="noopener noreferrer"&gt;What's in which Python — Ned Batchelder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Deploy a Next.js App on a VPS the Production Way</title>
      <dc:creator>Mahmud Hasan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 06:00:45 +0000</pubDate>
      <link>https://dev.to/mahmudhasan/how-to-deploy-a-nextjs-app-on-a-vps-the-production-way-42mm</link>
      <guid>https://dev.to/mahmudhasan/how-to-deploy-a-nextjs-app-on-a-vps-the-production-way-42mm</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.mahmudhasan.pro/blog/how-to-deploy-nextjs-app-on-vps" rel="noopener noreferrer"&gt;Byte by Mahmud&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most guides teach you to deploy a Next.js app on a VPS in five commands. The commands work — what's missing are the five rules you only learn when the same deploy has to survive a year of production: the build verification nobody does, the env var that demands a rebuild, the restart that must happen last. Here are both.&lt;/p&gt;

&lt;h2&gt;Step 1 — Get the right Node on the box&lt;/h2&gt;

&lt;p&gt;Next.js 16 needs Node 20.9 or newer, and Node 22 LTS is the sane default. Ubuntu's apt repositories ship an ancient Node that will either refuse to run your build or fail in weird ways halfway through it, so install from NodeSource instead:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v  # should print v22.x&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If the VPS carries global &lt;code&gt;NODE_ENV=production&lt;/code&gt; — some managed boxes do — plain &lt;code&gt;npm install&lt;/code&gt; will quietly skip your devDependencies, and the build dies with a missing-package error that looks nothing like the real cause. Run &lt;code&gt;npm install --include=dev&lt;/code&gt; in that case, or just use &lt;code&gt;npm ci&lt;/code&gt;, which is what you want on a server anyway: it installs exactly what the lockfile says, so the box matches your laptop.&lt;/p&gt;

&lt;h2&gt;Step 2 — The build is where VPS deploys go wrong&lt;/h2&gt;

&lt;p&gt;This is where most guides get vague and most deploys go wrong. The sequence is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git pull
npm ci
npm run build&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then &lt;strong&gt;wait for the build to exit and confirm &lt;code&gt;.next/BUILD_ID&lt;/code&gt; exists&lt;/strong&gt; before touching anything else. If you restart the process manager while the build is still writing, you get a half-written &lt;code&gt;.next&lt;/code&gt; directory — the app starts, serves your &lt;em&gt;old&lt;/em&gt; version, and you spend an hour convinced the deploy "didn't take." Check the exit code first. It's boring advice and it has saved more deploys than any other single habit.&lt;/p&gt;

&lt;p&gt;Two more build truths nobody puts in the quickstart:&lt;/p&gt;

&lt;p&gt;First, anything prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; is inlined into the JavaScript bundle at build time. It is not read at runtime. If you change a &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt;, you must rebuild — a restart won't pick it up. (Server-only variables, no prefix, only need a restart.) Mixing these up is the most common cause of "my new key isn't working."&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;next build&lt;/code&gt; is memory-hungry. On a 1 GB box it can get OOM-killed mid-build, leaving a stale &lt;code&gt;.next&lt;/code&gt; that serves yesterday's site. Either build on a box with 2 GB or more, or add a 2 GB swap file before building. A killed build's worst symptom is silence — the old version keeps running, so nobody notices.&lt;/p&gt;

&lt;h2&gt;Step 3 — Keep it alive with PM2&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;npm run start&lt;/code&gt; in your SSH session works until the session closes, which is approximately four minutes after you log off. PM2 is the process manager:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo npm install -g pm2
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup   # run the command it prints, then pm2 save again&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That last line is the reboot insurance — it writes a systemd service so your app comes back after a restart. Then test it: &lt;code&gt;pm2 list&lt;/code&gt; shows the process, &lt;code&gt;pm2 logs myapp&lt;/code&gt; shows the output, and a rising restart count means a crash loop (check the logs, not your config).&lt;/p&gt;

&lt;p&gt;The default port is 3000, but one app per port is the rule. A second app needs a different one: &lt;code&gt;pm2 start npm --name "app2" -- start -- -p 3001&lt;/code&gt;, and your Nginx config has to point at the right port. "Port already in use" errors are almost always a stale process from a previous deploy that nobody killed.&lt;/p&gt;

&lt;p&gt;Your update routine, forever: &lt;code&gt;git pull &amp;amp;&amp;amp; npm ci &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; pm2 restart myapp&lt;/code&gt;. Builds, then restarts. Never in the same command chain — let the build finish first.&lt;/p&gt;

&lt;h2&gt;Step 4 — Nginx in front, Certbot on top&lt;/h2&gt;

&lt;p&gt;Never expose port 3000 to the internet. Nginx accepts public traffic on 80 and 443 and forwards it to Node. The config is small but the headers matter:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name yourdomain.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Skip &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; and Next.js can't tell HTTPS from HTTP behind the proxy, which produces redirect loops and broken absolute URLs — the kind of bug that only exists on the server, never locally.&lt;/p&gt;

&lt;p&gt;Then SSL, which is the easiest part:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
sudo certbot renew --dry-run&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Certbot rewrites the Nginx config and sets up auto-renewal. The dry run confirms renewal will work without you finding out the hard way at 90 days.&lt;/p&gt;

&lt;p&gt;Don't forget the firewall. Open 22, 80, and 443; close 3000. The app should only be reachable through Nginx. (If you haven't locked down the server yet, &lt;a href="https://www.mahmudhasan.pro/blog/how-to-secure-linux-vps" rel="noopener noreferrer"&gt;do the 30-minute VPS hardening&lt;/a&gt; before this goes anywhere near production.)&lt;/p&gt;

&lt;h2&gt;Step 5 — What breaks after your Next.js VPS deploy goes live&lt;/h2&gt;

&lt;p&gt;Once the chain is running, it runs. What breaks is the stuff around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 502 from Nginx&lt;/strong&gt; is never an Nginx bug — it's a message that something behind it died. Check &lt;code&gt;pm2 logs&lt;/code&gt; first, then the Nginx error log. Restarting Nginx fixes nothing. (I wrote a &lt;a href="https://www.mahmudhasan.pro/blog/how-to-fix-502-bad-gateway-nginx" rel="noopener noreferrer"&gt;full walkthrough of reading a 502&lt;/a&gt; if you want the whole diagnostic order.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New files under &lt;code&gt;public/&lt;/code&gt; sometimes don't serve&lt;/strong&gt; after a deploy because the running &lt;code&gt;next-server&lt;/code&gt; caches the build manifest at startup. A &lt;code&gt;pm2 restart&lt;/code&gt; after adding covers or images is the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale &lt;code&gt;.next&lt;/code&gt; after a killed build&lt;/strong&gt; serves the old version silently. Always confirm the build exit code and &lt;code&gt;BUILD_ID&lt;/code&gt; before restarting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch memory over days.&lt;/strong&gt; ISR caching grows the process over time; &lt;code&gt;pm2 monit&lt;/code&gt; and a one-line health check are all the monitoring a small app needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets out of the client bundle.&lt;/strong&gt; Only variables prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; should ever reach the browser. The official Next.js security checklist says the same: env files belong in &lt;code&gt;.gitignore&lt;/code&gt;, public prefix only for public values.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;The honest cost&lt;/h2&gt;

&lt;p&gt;A $6–22/month VPS runs a Next.js app, a database, and the reverse proxy side by side, with the same bill in January and December. The trade is one afternoon of setup and you owning the ops: logs, shell, and the power to fix things yourself instead of filing a ticket. For a throwaway demo, a managed platform is faster. For anything you'll run for a year, the VPS math wins — and the steps above are the whole of it: Node, build carefully, PM2, Nginx, SSL, and the firewall. Everything else is debugging.&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.hostaccent.com/blog/how-to-host-nextjs-app-vps" rel="noopener noreferrer"&gt;How to Host Nextjs App on a VPS in 2026 (vs Serverless)&lt;/a&gt; — Hostaccent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/dopescripts/deploy-nextjs-on-a-vps-git-nginx-pm2-production-setup-2026-guide-6aa"&gt;Deploy Next.js on a VPS: Git, Nginx, PM2 &amp;amp; Production Setup (2026 Guide)&lt;/a&gt; — dev.to&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/building-your-application/deploying" rel="noopener noreferrer"&gt;Deploying&lt;/a&gt; — Next.js official docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/guides/production-checklist" rel="noopener noreferrer"&gt;Production Checklist&lt;/a&gt; — Next.js official docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arcjet.com/learn/self-host-nextjs-docker-security" rel="noopener noreferrer"&gt;Security advice for self-hosting Next.js in Docker&lt;/a&gt; — Arcjet&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.dlhck.com/essays/the-complete-guide-to-self-hosting-nextjs-at-scale" rel="noopener noreferrer"&gt;The Complete Guide to Self-Hosting Next.js at Scale&lt;/a&gt; — dlhck.com&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>vps</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
