Node.js Advanced: Security, Performance and Production
Event Loop, Streams and Workers
In this lesson you will
- Describe the event loop and the order of microtasks, timers and ticks
- Process data in chunks with streams and pipeline
- Use worker threads for CPU-heavy work
Introduction
Node runs your JavaScript on one thread. That is why a long calculation can freeze a whole server, and why understanding the event loop matters. This lesson explains how work is scheduled, how to handle large data without running out of memory, and how to use more than one CPU core.
The event loop
When your program starts, Node runs your script, then enters a loop that handles events: timers, network data, file results. Slow operations such as file and network I/O are handed to the operating system, and their callbacks are queued for when they finish. Your own JavaScript only ever runs one piece at a time.
Different queues have different priority. In a CommonJS script:
A real run prints:
sync
tick
promise
timeout- Synchronous code runs first.
process.nextTickcallbacks run next.- Promise callbacks (microtasks) run after that.
- Timers run later, in a later turn of the loop.
In an ES module the order of tick and promise can differ, because the module itself runs inside a promise. The lesson: do not depend on subtle ordering. Use promises and await.
Quick quiz
Why can a long synchronous calculation freeze a Node server?
Do not block the loop
Avoid on the main thread: huge loops, heavy JSON parsing of very large data, synchronous file functions in request handlers, and CPU-heavy work such as image processing or password hashing with synchronous functions. They delay every request.
Streams: data in chunks
Reading a huge file entirely into memory can crash your server. Streams process data piece by piece. They also let you start sending data before it is all ready.
Here is a small, real example that sends values through a transformation and collects the result:
ABCUse pipeline (not manual piping) because it handles errors and cleans up properly. Real uses include copying large files, compressing data and sending big responses: for example pipeline(createReadStream("big.csv"), res). Streams also provide backpressure, which slows the source when the destination cannot keep up, so memory stays under control.
Worker threads
To use more CPU cores, or to move heavy computation away from the main thread, use worker threads:
499999500000The worker runs in its own thread, and messages are copied between threads. In real projects, put the worker code in a separate file and pass the file path. For many tasks, create a pool of workers instead of a new one each time, since starting a worker costs time.
Other ways to scale: run several Node processes (with a process manager, containers or the cluster module) behind a load balancer, and move heavy work to a job queue with separate worker services.
When to use which
- I/O waiting (databases, APIs, files): normal async code is ideal.
- Large data: use streams.
- CPU-heavy computation: worker threads or a separate service.
- More capacity: more processes or containers.
Try it yourself
Write a script with a loop that computes for one second on the main thread while a timer logs every 100 ms. Notice the timer freezes. Move the loop into a worker and watch the timer keep ticking.
Summary
- Node runs your JavaScript on one thread, so blocking work delays everything.
- Order is: synchronous code, nextTick, promises, then timers, and you should rely on async and await, not on subtle ordering.
- Use streams for large data, worker threads for CPU-heavy tasks, and more processes for more capacity.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading