🚀 How to Build Scalable Email Verification in Node.js Like Real Apps Do

Checkout the Devflow - Devflow
Introduction
Most beginner tutorials send emails directly inside route handlers.
It works, but it’s not scalable.
What if the email server is slow? What if you're handling 1,000 requests per minute?
In this blog, I’ll show you how I built an email OTP verification flow like real-world apps — using Node.js, Redis, bcrypt, and BullMQ queue workers.
Just like how Dev.to, Hashnode, or Slack verify your email — but coded from scratch 💻
Why Email Verification Matters
Stops spam accounts
Confirms user identity
Used in signup, password reset, 2FA, and onboarding
Whether you're building a SaaS app or a side project, email verification is a must-have step.
How Email Verification Works (Behind the Scenes)
Here’s the step-by-step breakdown:
1. User clicks “Verify Email”
2. Backend generates a random 6-digit OTP
3. OTP is hashed with bcrypt
4. Hashed OTP is stored in Redis (with 5-min expiry)
5. OTP is queued using BullMQ to be emailed
6. Email gets delivered to the user
7. User submits the OTP for verification
8. Backend retrieves the hashed OTP from Redis
9. If it matches → User is marked isVerified: true
This is NOT login — it’s just email identity check.
Tech Stack Used
Tech | Purpose |
Node.js + Express | REST API |
Redis | Temporary storage of OTP |
bcrypt | Securely hash OTP |
BullMQ | Queue system to handle async emails |
Nodemailer / Resend | Send OTP via email |
Step-by-Step Implementation
1. Generate & Store OTP in Redis
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const hashedOtp = await bcrypt.hash(otp, 10);
await redisClient.set(email, hashedOtp, 'EX', 300); // expires in 5 min
Why bcrypt
? Even if Redis is exposed, OTPs stay secure
Why Redis? Fast, temporary, auto-expiring memory store
2. Queue the Email via BullMQ
await emailQueue.add('sendOtpEmail', { email, otp });
Email is sent asynchronously
Backend doesn't wait for the email to send
This keeps your server fast and responsive
3. Process the Queue and Send the OTP Email
emailQueue.process('sendOtpEmail', async (job) => {
const { email, otp } = job.data;
await sendEmail(email, otp); // Use Resend, SendGrid, Nodemailer
});
Retry failed jobs
Monitor email logs
Use multiple workers at scale
4. Verify OTP
const storedOtp = await redisClient.get(email);
if (!storedOtp) return res.status(400).json({ error: "OTP expired" });
const isValid = await bcrypt.compare(userOtp, storedOtp);
if (!isValid) return res.status(400).json({ error: "Invalid OTP" });
await db.user.update({
where: { email },
data: { isVerified: true },
});
This step completes the verification.
Now the user has isVerified: true
in their profile.
Real-World Security Practices
Hash OTPs — don’t store them as plain text
Expire OTPs — use Redis TTL
Rate limit OTP requests (to avoid spam/flooding)
Never log OTPs
Use verified sender emails with proper headers (SPF, DKIM)
Diagram of the Architecture
Final Thoughts
This is how real-world products verify email without blocking the backend or compromising on speed and security.
OTPs sent in the background
Hashed and temporary
Built for scale
TL;DR
Email verification is a common user onboarding step
Do NOT send emails directly in route handlers
Use Bull queue + Redis to make it efficient
bcrypt protects the OTP
Simple, secure, and scalable
Follow My Dev Journey
I’m building a real-world Q&A platform like Quora — DevFlow — in public!
Follow me on Twitter for daily dev insights
x/sangammundhe
#NodeJS #EmailVerification #MERNStack #BuildInPublic #BackendDev #SystemDesign #Redis #BullMQ #100DaysOfCode #WebDev
Subscribe to my newsletter
Read articles from Sangam Mundhe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
