Muhammad Tabarak - Home
Backend Engineering

Large CSV Import Pipelines in Next.js Without Server Timeouts

Building memory-efficient, non-blocking CSV import pipelines in Next.js and Node.js using backpressure streams and worker batching.

Muhammad Tabarak
Muhammad TabarakFounder & Software Engineer
November 2, 2026
3 min read
Large CSV Import Pipelines in Next.js Without Server Timeouts

Bulk data onboarding is one of the most common requirements in SaaS applications—and one of the quickest ways to crash a Node.js server. On Curved Lab, enterprise agency managers regularly import creator rosters and historical campaign performance sheets containing tens of thousands of rows.

In our earliest implementation, uploading a 50MB CSV file triggered out-of-memory (OOM) fatal crashes. The server attempted to parse the entire file into a JavaScript array of objects in memory, exhausting the V8 heap and freezing the Node.js event loop. Here is how we redesigned the pipeline using streaming backpressure and chunked database batching to handle 500,000+ rows effortlessly with sub-50MB RAM consumption.

The Memory Bottleneck: Why Buffering Fails

When you read a file with fs.readFileSync() or parse it via standard JSON converters, every single row is transformed into a full V8 object with prototype chains and memory overhead. A 50MB raw CSV file can easily balloon to over 800MB of resident memory in V8. Under concurrent imports, the garbage collector pauses the main thread before V8 terminates with a heap exhaustion error.

Memory Comparison
Rendering interactive flow...
Figure: Memory Profile: In-Memory Buffering vs Streaming Backpressure

Implementing Stream Piping with Backpressure

Streaming solves the memory consumption problem, but introduces a new danger: speed mismatch. A disk read or network stream can parse 5,000 rows per second, while your database bulk insert might only process 1,000 rows per second. Without backpressure, unwritten rows accumulate in memory buffers, eventually producing the exact same OOM crash.

We solved this by implementing a custom Node.js Transform stream that monitors downstream database acknowledgment. When the internal queue reaches a threshold (e.g. 1,000 records), the stream pauses reading from the file until the database bulkWrite() completes and signals a drain event.

Pipeline Architecture
Rendering interactive flow...
Figure: End-to-End Streaming Pipeline with Row-Level Quarantine

Zero-Downtime Reliability Gains

  • Flat Memory Footprint: Whether processing a 500-row test sheet or a 250,000-row enterprise roster, Node.js resident memory stays under 50MB throughout the entire operation.
  • Row-Level Error Quarantine: A single malformed email or negative payout number on row 4,821 does not abort the entire import. Valid rows are committed while invalid rows are isolated into an error report returned immediately to the user.
  • Responsive UI Progress: Using Server-Sent Events (SSE), the Next.js frontend displays real-time progress bars and processed row counts directly to the user as chunks are confirmed by the database.
Enjoyed this article?Add this site as a preferred source so Google surfaces my posts first — in Search & AI Mode.
Add as preferred source→