Contents
TABLE OF CONTENTS
All posts
Building a background job processing system using Node.js and Redis.
Anshuman Praharaj

Building a background job processing system using Node.js and Redis.

Over the last few weeks while learning more about distributed systems, I decided to build a background job processing system using Node.js…


Over the last few weeks while learning more about distributed systems, I decided to build a background job processing system using Node.js and Redis.

The idea was pretty simple.

Whenever a client performs an action that doesn't need an immediate response, instead of handling everything inside the API request itself, the API creates a job and pushes it into Redis. Worker processes then pick up these jobs and execute them asynchronously in the background.

Something like:

```Client → API → Redis Queue → Worker → Processing Complete```

At first, it felt like a fairly straightforward project.

Push jobs into a queue, have workers consume them, process the work, done.

But very quickly I realized that the queue itself isn't the interesting part.

The real challenge is reliability.

Background jobs are usually responsible for important operations. Sending password reset emails, generating invoices, processing uploads, syncing data with third-party services and a lot more.

If one of these jobs gets dropped, nobody notices the queue failed.

They only notice that the email never arrived or their invoice was never generated.

And in distributed systems, failures aren't edge cases. They're expected.

Workers crash. Redis connections drop. External APIs timeout. Services get rate limited. Networks become unreliable.

So instead of focusing only on getting jobs from point A to point B, I started focusing on what happens when things go wrong.

I implemented a retry mechanism that catches failures, decreases the retry count and safely re-enqueues jobs for another attempt.

I also built a concurrent worker pool using Redis BLPOP, which allows workers to block and wait for jobs instead of continuously polling Redis. This reduced unnecessary load while making it easy to scale processing horizontally by simply adding more worker instances.

The biggest takeaway from this project was that asynchronous systems aren't really about speed.

They're about resilience.

Decoupling expensive work from user-facing APIs is useful, but designing systems that continue working even when parts of the infrastructure fail is where most of the engineering effort actually goes.

A few lessons I walked away with:

• Design for failure from day one. Something will eventually crash.

• Retries are not optional when dealing with distributed systems.

• Polling wastes resources when blocking primitives can do the same job more efficiently.

• Every retry mechanism needs limits, otherwise bad jobs can clog the entire system forever.

Building this project gave me a much better appreciation for distributed systems.

Moving data from one place to another is easy.

Making sure it eventually gets there despite failures is the hard part.