To explain this, we first need to understand how web requests are usually handled.
For example, let’s pick PHP.
Let’s say you have a registration page. After the user clicks the “register page”, the following happens:
- It checks if a user with that e-mail already exists on the database
- It creates a user in the database
- It sends a welcome e-mail to the user Then it redirects the user to the dashboard
The code itself should be pretty fast, but it depends on other resources: it has two operations that depend on the database and one operation that relies on an external service (Mailgun, Postmark, Amazon SES, whatever).
That means the code is synchronous. That is, you have to wait until each operation finishes before going to the next.
That also means that a process will hang until all the operations are completed.
Let’s assume the whole process takes 5 seconds (i’m exaggerating here) — while that is happening, the server is blocked. It won’t be able to handle another request until that previous one finish.
To get around this, a new process would be spawned to handle that second request. If in the meanwhile a third request was received, another process would be spawned. That is valid to all of the app’s routes, so if a user is requesting another page, it would also need another process. It cannot be handled simultaneously.
You can check this very quickly by running
top on your server and clicking on multiple links, specially if they’re heavy. You’ll see processes spawning.
In short, the code itself does not take long to execute, but it depends on external resources and it’s going to hang until those are finished. It is synchronous.
For most applications, this is not an issue at all since database calls tend to be very fast and external things like making a request to an API (to send an e-mail, in this case) can be pushed to a queue and handled separately.
Now, Node is a bit different. It manages to run each request in parallel through non-blocking I/O.
What Node does is it manages to handle other requests while you’re waiting for an I/O operation, which is often expensive .
For instance, if our database were to take a few seconds to respond, all the requests would get blocked, but you have the ability to pass a callback to the DB call and let Node know that it should not block the server and just resolve the callback once the operation gets completed.
For instance, here’s an example of async/non-blocking code:
console.log('first'); setTimeout(() => console.log('second'), 1000); console.log('third');
If you run the following you’ll see that you’ll get
third right away, but you’ll only
second a little bit later.
So that’s basically what happens in the back-end. While you have an expensive I/O operation, Node is simply going to handle other requests and come back to the others once the operation finishes. That is how it handles multiple requests at the same time in a non-blocking fashion.
Of course, there are other options such as Erlang’s concurrency model that is used by Elixir, which is far more robust and stable, but that’s content for another post…
If you’ve liked this post, make sure to follow me on twitter to check what else I’m posting :-)