Most of the server-side languages, like PHP, ASP.NET, Ruby, JAVA servers, follow multi-threaded architecture. That means, every request by the client results in the instantiation of a new thread or even a process.
However, in Node.js, all requests are handled in a single thread with shared resources. Then how does Node.js handle concurrent traffic or requests? It follows βSingle Threaded Event Loop Modelβ architecture that runs on top of a single V8 engine instance.
Node.js is event-driven that implements background workers to achieve non-blocking asynchronous behavior. We called it the Observer Pattern. Node thread keeps an event loop and whenever a task gets completed, it fires the corresponding event which signals the event-listener function to execute as illustrated below.
As soon as Node.js starts, it initializes the event loop, processes the provided input script (i.e. initiates variables and declares functions) which may make async API calls, schedule timers, or call process.nextTick()
, then begins processing the event loop.
As shown in the figure above, each phase has a FIFO queue of callbacks to be executed.
Overview of the Phases:
timers: this phase executes callbacks scheduled by setTimeout()
and setInterval()
.
pending callbacks: executes I/O callbacks deferred to the next loop iteration.
idle, prepare: only used internally.
poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()
); NodeJs will block here when appropriate.
check: setImmediate()
callbacks are invoked here.
close callbacks: some close callbacks, e.g. socket.on('close', ...)
.
More detail on this can be read from the official docs.
As shown in the above block diagram, Node.js listens and passes every concurrent traffic in a queue, which will be executed by an event loop as explained above. Letβs see an example to observe this single-threaded architecture of a Node.js web application.
const app = express()
let visitorCount = 0
app.get("/", (req, res, next) => {
visitorCount++
res.send(`Hello World, visitor counter is: ${visitorCount}`)
})
const port = 8002
app.listen(port, () => {
console.log(`Start listening at port: ${port}`)
})
In the above example, we are using express-js
which we need to install from npm.
To run the above script, simply type the following command in your terminal.
$ node server.js // here, server.js is the name of the file
Now, if we browse localhost:8002
in the browser, on every request, the visitorCount
gets updated. Isn't that magic? In other programming languages, to achieve this, we will need to store that counter in some persistent storage. Here, as per the output, on every request, the visitorCount
variable is being updated. That means, for all requests, Node.js is running the same instance (thread/process) and visitorCount
variable is the same for all the requests.
This is how Node.js works. Due to all these architectural implementations of Observer patterns, Node.js is pretty fast compared to similar other server-side languages and technologies.
This article was originally published in YIPL Blog.
Top comments (2)
Great post..
Thanks :)