Node.js's bottleneck in email validation
Email validation at scale is not trivial. For each address you need at least three checks: syntax, domain existence via DNS/MX, and mailbox verifiability via SMTP.
The initial engine was in Node.js with async/await and Promise.all. It worked fine with small lists, but when scaling to thousands of emails, the event loop showed inconsistent latencies. With 5000 emails in parallel, Node.js opened too many simultaneous SMTP connections and remote servers rejected them with 421 errors (too many connections).
Why Go
Go had goroutines as a native concurrency primitive, channels for communication, efficient garbage collector, static binary compilation, and a complete standard library for HTTP and DNS. Learning curve for someone with backend experience: days, not weeks.
Go's concurrency model
The Go solution uses a worker pool with a semaphore to control maximum concurrency. What Node.js did with Promise.all, Go does with goroutines and semaphores: exactly the concurrency level you want, no more, no less.
The SSE pivot due to Caddy
The original plan was to stream results via SSE from the Go server. The problem: Caddy buffers HTTP responses before sending them to the client, completely destroying the streaming experience.
We changed the frontend architecture: the Go backend processes the complete list and returns results in batches, while the frontend simulates the streaming effect with client-side animations using setTimeout. The result is visually indistinguishable from real SSE.
Real production performance
After the Go migration, throughput with 200 concurrent goroutines processing 10,000-email lists dropped total time from minutes to seconds. MX + SMTP verification per domain landed under 2 seconds at the 95th percentile.
