Weβve all been thereβhitting that βDownload CSVβ button and watching the browser hang for what feels like forever. If your app is generating large datasets on the fly over an HTTP request, youβre probably familiar with the frustration: timeouts, memory spikes, slow performance, and a horrible user experience.
At tejaya.tech, we believe thereβs a better way. Letβs dive into how you can offload large CSV generation to a background job using queues, and notify users via email when itβs ready. Smooth, efficient, and user-friendly.
π₯ The Problem with Direct Downloads (Synchronous HTTP)
In a typical setup, when a user clicks to download a CSV:
The request hits your server.
You generate the CSV in-memory.
You stream the file back to the browser.
Sounds simple, right? But as data grows, this can get ugly.
β Drawbacks:
Request timeouts β Most web servers and browsers canβt handle requests lasting more than 30β60 seconds.
Memory bloat β Holding massive datasets in memory is a recipe for disaster, especially if multiple users do it simultaneously.
Poor UX β Users are stuck staring at a spinner with no idea when itβll finishβor if itβll crash.
β The Better Way: Background Jobs + Queues
Instead of tying up server resources and the user's browser, offload heavy lifting to a background worker.
Hereβs how the flow works:
User triggers the CSV download request.
Backend enqueues a background job to generate the file (using a queue like Redis + Sidekiq, BullMQ, Celery, etc.).
The worker generates the CSV in the background, saves it (e.g., in S3 or local storage).
Once done, the system emails the user with a secure link to download the file.
This pattern is used by major apps like Shopify, Stripe, and GitHub for heavy report exports.
π οΈ Implementation Overview
Letβs break it down.
1. Queue the Job
// Example in Node.js using BullMQ
import { Queue } from 'bullmq';
const csvQueue = new Queue('csv-generation');
app.post('/download-report', async (req, res) => {
const job = await csvQueue.add('generateCSV', { userId: req.user.id });
res.status(202).send('Weβll email you when itβs ready!');
});
2. Background Worker
import { Worker } from 'bullmq';
const worker = new Worker('csv-generation', async job => {
const data = await fetchUserData(job.data.userId);
const filePath = await generateCSV(data);
await sendEmailWithDownloadLink(job.data.userId, filePath);
});
3. Email Notification
You can use SendGrid, Postmark, SES, etc. to send a secure link:
Subject: Your CSV report is ready!
Hey there,
Your data export is ready.
π [Download Now](https://yourdomain.com/downloads/file.csv)
This link will expire in 24 hours.
β‘ Benefits of the Background Job Pattern
Feature | Synchronous (HTTP) | Asynchronous (Background Job) |
|---|---|---|
Timeouts | β Risky | β Safe |
Memory Load | β High | β Minimal |
User Experience | π© Blocking | π Seamless |
Scalability | π« Painful | β Scalable |
Error Handling | π€·ββοΈ Tricky | π§ Retryable, trackable |
π§ Pro Tips
Use UUIDs or signed URLs for secure download links.
Expire download links after a certain period.
Show job status in your UI (e.g., βGenerating reportβ¦β with polling or websockets).
Log and monitor queue failures (e.g., Sentry, Prometheus).
Chunk large data while generating CSVs to avoid memory spikes.
π¬ Final Thoughts
Handling large file downloads asynchronously isnβt just about performanceβitβs about respecting your userβs time. By embracing queues and background workers, you can provide a smoother, faster, and more reliable experience.
Thinking of implementing this in your product? Letβs nerd out about architecture or share lessons learnedβhit reply or drop a note on tejaya.tech!
Let us know if you want to Implement similiar workflow for your application, or tailor this for a specific audience like product teams or backend engineers.

