<?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: Hamed Mohamed</title>
    <description>The latest articles on DEV Community by Hamed Mohamed (@7amed3li).</description>
    <link>https://dev.to/7amed3li</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3744294%2F73294918-b27b-4724-bd82-da60ce70202c.png</url>
      <title>DEV Community: Hamed Mohamed</title>
      <link>https://dev.to/7amed3li</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/7amed3li"/>
    <language>en</language>
    <item>
      <title>How I Stopped Node.js from Freezing While Bulk-Processing 1,500+ Excel Rows</title>
      <dc:creator>Hamed Mohamed</dc:creator>
      <pubDate>Sun, 31 May 2026 08:51:58 +0000</pubDate>
      <link>https://dev.to/7amed3li/how-i-stopped-nodejs-from-freezing-while-bulk-processing-1500-excel-rows-2983</link>
      <guid>https://dev.to/7amed3li/how-i-stopped-nodejs-from-freezing-while-bulk-processing-1500-excel-rows-2983</guid>
      <description>&lt;p&gt;I was building a virtual attendance tracking system for a university. The client requested a feature we’ve all built a hundred times: &lt;strong&gt;"Allow admins to upload an Excel file to bulk-import students."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Easy, right? I set up &lt;code&gt;multer&lt;/code&gt;, grabbed the file buffer, parsed it, looped over the rows, and inserted them into Postgres. &lt;/p&gt;

&lt;p&gt;Locally, with a dummy file of 5 rows, it was blazingly fast. We shipped it to production.&lt;/p&gt;

&lt;p&gt;The next day, an admin uploaded a real-world file containing &lt;strong&gt;1,500 students&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;The server choked. The API timed out. The database connection pool was entirely exhausted, and every other user trying to use the app got a spinning wheel of death. 💀&lt;/p&gt;

&lt;p&gt;Here is exactly how I diagnosed the disaster and optimized the endpoint to handle thousands of rows efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚩 The "Death Loop" (What I did wrong)
&lt;/h3&gt;

&lt;p&gt;When you look closely at the initial code, the problem wasn't parsing the Excel file. The problem was &lt;strong&gt;I/O and network abuse inside a loop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a simplified version of my initial crime:&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;// ❌ Don't do this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;excelParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileBuffer&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;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Hash password (CPU intensive)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashedPassword&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;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Insert into database&lt;/span&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`INSERT INTO users ... RETURNING id`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`INSERT INTO enrollments ...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Send welcome email (Slow external API)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&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;Let's do the math:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bcrypt.hash&lt;/code&gt; running sequentially 1,500 times puts pressure on the libuv threadpool.&lt;/li&gt;
&lt;li&gt;Email SMTP takes about &lt;code&gt;300ms - 500ms&lt;/code&gt; per send. &lt;code&gt;500ms * 1,500 = 750 seconds&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was holding database connections open while waiting for an external Email API to respond for &lt;strong&gt;12+ minutes&lt;/strong&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  🛠️ The Architecture Refactor
&lt;/h3&gt;

&lt;p&gt;I needed to separate the slow external operations from the database operations. But I also faced the &lt;strong&gt;"All-or-Nothing" Dilemma&lt;/strong&gt;: If row 1,499 fails because of a duplicate ID, I cannot let the entire transaction fail and reject the 1,498 good rows.&lt;/p&gt;

&lt;p&gt;Here is the 3-step solution that fixed it.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Grouping DB Work
&lt;/h4&gt;

&lt;p&gt;Instead of keeping many small DB operations mixed with slow external calls, I grouped the database work into &lt;strong&gt;one controlled transaction&lt;/strong&gt;. The goal was not just fewer queries, but a radically shorter connection hold time.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Row-Level &lt;code&gt;SAVEPOINT&lt;/code&gt;s
&lt;/h4&gt;

&lt;p&gt;To handle partial failures gracefully, I used raw SQL &lt;code&gt;SAVEPOINT&lt;/code&gt;s inside the transaction. If a specific row throws an error, I rollback &lt;em&gt;only&lt;/em&gt; to that row's savepoint. The rest of the batch survives.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Decoupling Emails
&lt;/h4&gt;

&lt;p&gt;Sending emails inside a DB transaction is a cardinal sin. I created an in-memory array &lt;code&gt;pendingEmails&lt;/code&gt;. We push data to it during the loop, and only process the slow SMTP calls &lt;em&gt;after&lt;/em&gt; the database transaction safely commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Final Code
&lt;/h3&gt;

&lt;p&gt;Here is the battle-tested version:&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;// ✅ The Optimized Way&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;excelParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileBuffer&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;pendingEmails&lt;/span&gt; &lt;span class="o"&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;errors&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;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rows&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="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savepointName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`sp_row_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Safe here because savepointName is generated internally, not from user input&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$executeRawUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SAVEPOINT &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;savepointName&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="nx"&gt;hashedPassword&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;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`INSERT INTO users ...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`INSERT INTO enrollments ...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Queue email data (DO NOT send yet)&lt;/span&gt;
      &lt;span class="nx"&gt;pendingEmails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plainPass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Implicitly commit this row&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$executeRawUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`RELEASE SAVEPOINT &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;savepointName&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="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="o"&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// If THIS row fails, rollback ONLY this row. Loop continues!&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$executeRawUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`ROLLBACK TO SAVEPOINT &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;savepointName&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="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;results&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="c1"&gt;// The DB connection is now released and returned to the pool!&lt;/span&gt;
&lt;span class="c1"&gt;// Now we safely fire off the slow SMTP emails asynchronously.&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;mail&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pendingEmails&lt;/span&gt;&lt;span class="p"&gt;)&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;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plainPass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;** Security Note on Passwords:**&lt;br&gt;
In the code above, the legacy system required emailing the generated temporary password. &lt;strong&gt;In a modern production system, emailing plain passwords is an anti-pattern.&lt;/strong&gt; A much safer approach is saving the user without a password and pushing an &lt;code&gt;activationToken&lt;/code&gt; to the queue, emailing them a secure &lt;em&gt;magic link&lt;/em&gt; to set their own password.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Results
&lt;/h3&gt;

&lt;p&gt;The difference was night and day. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database Write Time:&lt;/strong&gt; Database write operations dropped to around &lt;strong&gt;~1.5 seconds&lt;/strong&gt; after isolating them from the slow SMTP overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Responsiveness:&lt;/strong&gt; The server remained responsive because long-running SMTP calls were no longer holding database connections open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin UX:&lt;/strong&gt; The endpoint now returns a clean JSON array of exactly which rows failed (e.g., &lt;code&gt;row 42: invalid email&lt;/code&gt;), so the admin knows what to fix instead of getting a generic &lt;code&gt;500 Internal Server Error&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;When building bulk-import features in Node.js, your enemy isn't Node—it's how you manage external I/O. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Never put slow external APIs (like emails) inside a DB lock.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep connection hold times as short as possible.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume data is dirty&lt;/strong&gt; and design for partial failures using Savepoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Have you ever accidentally crashed a server with a bad loop? Let's hear your war stories in the comments! 👇&lt;/p&gt;

</description>
      <category>node</category>
      <category>backend</category>
      <category>database</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I thought refactoring a production backend was about clean code. I was wrong.</title>
      <dc:creator>Hamed Mohamed</dc:creator>
      <pubDate>Thu, 14 May 2026 10:36:46 +0000</pubDate>
      <link>https://dev.to/7amed3li/i-thought-refactoring-a-production-backend-was-about-clean-code-i-was-wrong-2p82</link>
      <guid>https://dev.to/7amed3li/i-thought-refactoring-a-production-backend-was-about-clean-code-i-was-wrong-2p82</guid>
      <description>&lt;p&gt;I thought refactoring our backend would mostly be about rewriting bad code.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;The hardest part was making sure nobody noticed the refactor was happening at all.&lt;/p&gt;

&lt;p&gt;I’ve been working on restructuring a production Express backend used for a university QR attendance system with 2000+ daily users.&lt;/p&gt;

&lt;p&gt;When I started digging through the codebase, I found things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;routes with 3000+ lines&lt;/li&gt;
&lt;li&gt;duplicated JWT middleware&lt;/li&gt;
&lt;li&gt;pg + knex + prisma all used together&lt;/li&gt;
&lt;li&gt;business logic directly inside routes&lt;/li&gt;
&lt;li&gt;runtime schema modifications&lt;/li&gt;
&lt;li&gt;multiple PrismaClient instances across route files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing that genuinely surprised me:&lt;/p&gt;

&lt;p&gt;Every route file creating its own &lt;code&gt;new PrismaClient()&lt;/code&gt; meant the app was opening multiple independent connection pools in production.&lt;/p&gt;

&lt;p&gt;The code fix itself was tiny.&lt;/p&gt;

&lt;p&gt;The production impact wasn’t.&lt;/p&gt;

&lt;p&gt;Another thing I learned during this refactor:&lt;br&gt;
“clean architecture” is the easy part compared to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserving backward compatibility&lt;/li&gt;
&lt;li&gt;not breaking the mobile app&lt;/li&gt;
&lt;li&gt;keeping response shapes identical&lt;/li&gt;
&lt;li&gt;introducing tests into fragile flows&lt;/li&gt;
&lt;li&gt;designing rollback paths before deploys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also started categorizing features by “refactor risk level”.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
I completely refused to touch attendance/session flows before writing integration tests first because those flows affect real student attendance records.&lt;/p&gt;

&lt;p&gt;One of the more interesting migrations was replacing long transactional &lt;code&gt;BEGIN / COMMIT / ROLLBACK&lt;/code&gt; flows with &lt;code&gt;prisma.$transaction&lt;/code&gt; while still preserving the exact same API behavior.&lt;/p&gt;

&lt;p&gt;The deeper I got into this project, the more backend engineering stopped feeling like:&lt;br&gt;
“building APIs”&lt;/p&gt;

&lt;p&gt;and started feeling more like:&lt;br&gt;
risk management under production constraints.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
