What Node JS is and how it works
Node JS is an open source runtime that executes JavaScript outside the web browser. It pairs Google's V8 engine, which compiles and runs the language, with a layer of C and C++ libraries that give a script access to files, sockets, processes, timers, and cryptography. That pairing is the whole idea: Node JS takes a language built for page interactivity and turns it into a general purpose tool for writing servers, command line programs, build pipelines, and background workers.
The design decision that shapes everything else is the event loop. Rather than dedicating an operating system thread to each connection, Node JS runs your JavaScript on one thread and hands slow work such as disk reads and network requests to the system underneath, collecting the results later. A single Node JS process can hold thousands of mostly idle connections without paying for thousands of call stacks, which is why the runtime became popular for APIs, proxies, and anything that spends its life waiting on other services.
Node JS first appeared in 2009 and is now developed in the open by a large contributor base under the OpenJS Foundation, on a published release calendar with long term support windows. Growing beside it was npm, the bundled package client and registry that turned third party code into a one line install. Most Node JS projects today are a small amount of application code sitting on a deliberately chosen set of dependencies.
This page covers the origins of the project, the mechanics of the loop and the thread pool, the two module systems, the core library, the ways to use more than one CPU core, the release calendar, and the practical work of running Node JS in production. Code samples are described rather than dumped, so the concepts stay portable across versions.
LANGUAGE ......... JavaScript, plus WebAssembly
ENGINE ........... V8
ASYNC LAYER ...... libuv
LICENSE .......... MIT
STEWARD .......... OpenJS Foundation
FIRST RELEASE .... 2009
PLATFORMS ........ Linux, macOS, Windows, and other Unix targets
[01]Origins and stewardship
Ryan Dahl introduced Node JS at JSConf EU in Berlin in November 2009, after a first release earlier that year. His argument was that most server frameworks of the era blocked on input and output by default, and that programmers therefore paid for concurrency in threads and locks. JavaScript, with its callbacks and its lack of an existing blocking I/O library, was an unusually clean host for an event driven model, and V8 was fast enough to make the idea practical.
A runtime is only as useful as the code you can reach from it. In 2010 Isaac Schlueter released npm, which gave Node JS a manifest format, a dependency resolver, and a public registry. Publishing became trivial, module boundaries stayed small, and the ecosystem grew faster than any single vendor could have planned. That growth is the reason people still reach for Node JS on projects that have nothing to do with web servers.
In late 2014 a group of contributors forked the project as io.js, frustrated with the pace of releases and with a governance model controlled by a single company. The fork shipped faster and pulled contributors with it, and the dispute ended in a merger: the code came back together in 2015 as version 4.0 under the newly formed Node.js Foundation, hosted by the Linux Foundation, with an open technical steering committee. Nearly every governance feature people now rely on, including the release calendar, dates from that reconciliation.
In 2019 the Node.js Foundation merged with the JS Foundation to create the OpenJS Foundation, which remains the home of the project. Day to day work is split among working groups covering releases, security, build infrastructure, diagnostics, and documentation, and any change large enough to affect users goes through public discussion. For a team betting a product on Node JS, that visible process matters as much as any benchmark, because it makes the support timeline predictable.
The project also left marks outside its own repository. libuv, the cross platform asynchronous I/O library written for Node JS, is now used by unrelated language runtimes, and the design of the module and package ecosystem influenced later JavaScript tooling. A general history of the project and its releases is maintained on Wikipedia for readers who want the chronology in one place.
[02]Architecture of the runtime
It helps to picture Node JS as three layers stacked on each other. At the bottom sits V8, which parses your JavaScript, compiles it to machine code, and manages the heap. Beside V8 sits libuv, which owns the event loop, the platform specific polling mechanisms, and a small thread pool. On top of both sits the part written in JavaScript: the core modules you import, implemented against C++ bindings that expose the lower layers.
What the loop actually does
The loop is a repeating sequence of phases, not a vague queue. Each turn moves through expired timers, pending system callbacks, the poll phase where the process waits for I/O events, the check phase where setImmediate callbacks run, and finally close callbacks for sockets and handles that have been torn down. Understanding the order explains most of the surprising scheduling questions people ask about Node JS.
- >timers run callbacks scheduled by
setTimeoutandsetIntervalwhose delay has elapsed. - >poll is where the process blocks, waiting for readable sockets, finished file operations, and other completions.
- >check runs
setImmediatecallbacks, which is the correct way to yield to the loop between chunks of work. - >close handles cleanup callbacks after a handle is destroyed.
Two queues sit outside the phases and drain between them. Callbacks registered with process.nextTick run first, then the microtask queue that resolves promises. Because both drain completely before the loop advances, a recursive stream of ticks or promise resolutions can starve timers and I/O entirely, which is one of the few genuinely counterintuitive traps in Node JS.
The thread pool nobody notices
Node JS is often called single threaded, which is true of your JavaScript and false of the process. libuv keeps a pool, four threads by default and adjustable with the UV_THREADPOOL_SIZE environment variable, that carries out work the operating system cannot do asynchronously in a portable way. File system calls, DNS lookups through dns.lookup, compression, and several cryptographic operations all go there.
The practical consequence is that a workload heavy in file reads or password hashing can exhaust the pool while sockets sit idle, producing latency that looks like a network problem. Raising the pool size or moving the work elsewhere fixes it. Network I/O, by contrast, does not use the pool at all, so a Node JS proxy handling many connections stays cheap.
Memory is managed by V8's garbage collector, with a bounded old space whose default depends on the platform and available RAM. Long lived Node JS services usually fail not from raw allocation speed but from retention: caches without eviction, listeners never removed, closures holding request bodies. The heap flag --max-old-space-size raises the ceiling, but it postpones a leak rather than curing one.
Asynchronous style has moved on considerably since the early years. Callbacks with an error first argument were the original convention, promises standardized composition, and async functions with await made asynchronous code read top to bottom. Modern Node JS core exposes promise flavored versions of most APIs, and util.promisify covers older interfaces you cannot change.
Any single synchronous block longer than a few milliseconds is a latency tax paid by every other request in flight. Measure before you optimize, but never leave a tight loop over a large array on the main thread of a busy service.
[03]Nothing waits in line
Every performance story in Node JS reduces to one sentence: the loop must keep turning. There is no scheduler that will preempt your function halfway through and give another request a chance. When you own the thread, you own the tail latency of the entire process, and no amount of horizontal scaling hides a handler that thinks for two hundred milliseconds.
This is also why the synchronous variants in the standard library carry warnings. Reading a configuration file with a blocking call at startup is fine and even sensible. Doing the same inside a request handler stalls every connection the Node JS process is holding, including health checks, which is how a slow disk turns into a restart loop.
Treat the rule as a design constraint rather than a limitation. Work that fits the model, coordinating services, streaming bytes, transforming records as they arrive, runs remarkably well on a modest machine. Work that does not fit, such as image processing or large numeric jobs, belongs in a worker thread, a separate process, or a different tool entirely, and Node JS gives you all three options.
[04]Modules and packages
Node JS predates standardized JavaScript modules, so it invented its own. CommonJS loads a file synchronously with require and exposes values by assigning to module.exports. Resolution walks up the directory tree through node_modules folders, caches each module after first load, and treats the file system as the namespace. Millions of published packages still ship this way.
ECMAScript modules are now the standard alternative and are fully supported. A package opts in with "type": "module" in its manifest, or a single file opts in with the .mjs extension, while .cjs forces the older format. ESM in Node JS is asynchronous, statically analyzable, and supports top level await. Recent releases also allow require of a module graph that has no top level await, which removed the sharpest edge of the transition.
Built in modules can be imported with the node: prefix, as in node:fs or node:path. The prefix is worth adopting everywhere: it makes the intent unambiguous to readers and tooling, and it cannot be shadowed by a package of the same name, which closes a small but real class of confusion attacks against Node JS projects.
Dependencies and the manifest
Every Node JS project is described by package.json: its name and version, its entry points, its scripts, and its dependency ranges expressed in semantic versioning. Installing writes a lockfile that pins the exact resolved tree so that a colleague, a CI runner, and a production image all get identical bytes. npm ships with Node JS, and pnpm and Yarn are drop in alternatives that reshape how packages are stored on disk.
Dependency discipline is the single highest leverage habit in Node JS work. Prefer fewer and larger dependencies from maintained sources, read what a package pulls in transitively before adding it, commit the lockfile, run audits as part of the pipeline, and keep install scripts under review since they execute code on your machine. The registry's openness is why the ecosystem is so productive and also why the supply chain deserves attention.
[05]The core library
A surprising amount of work needs no dependencies at all, because Node JS ships a broad standard library. There is file system access, path manipulation, HTTP and HTTPS clients and servers, raw TCP and UDP sockets, URL parsing, cryptography, compression, operating system information, process spawning, an event emitter, streams, binary buffers, worker threads, a test runner, and diagnostic hooks. Learning the core well reduces both your install size and your exposure.
Streams are the most characteristic part of the library and the least understood. A readable stream produces chunks, a writable stream consumes them, a transform sits between the two, and pipeline wires them together with proper error propagation and cleanup. The point is backpressure: a slow consumer tells a fast producer to pause, so a Node JS process can move a file larger than its own memory without buffering the whole thing.
The HTTP module is deliberately low level. It gives you request and response objects, keep alive handling, and headers, and it leaves routing, body parsing, and content negotiation to you. Most teams put a framework on top for ergonomics, and that is a reasonable default, but reading the raw server API once pays off when you need to debug a timeout, a header limit, or a hung connection in a live Node JS service.
Over recent years the core has absorbed a series of web platform standards, so code moves more easily between browsers and servers. Global fetch, URL, AbortController, structuredClone, Web Streams, and the standard timers promises API are all available. For newer Node JS projects, the browser flavored spelling of an operation is often the right one to reach for.
Tooling has moved inward too. The built in test runner in node:test covers describing suites, assertions, mocking, watch mode, and coverage reporting well enough for many codebases; node --watch restarts a process when files change; and an inspector protocol connects the same debugger and profiler used for browser JavaScript. A modern Node JS setup can be considerably smaller than one assembled five years ago.
[06]Using more than one core
The event loop makes I/O cheap and does nothing for computation. If a request spends its time parsing a large payload, resizing an image, or running a simulation, a single Node JS process will saturate one core while the rest of the machine idles. There are three established answers, and choosing between them is mostly about how much state needs to be shared.
Worker threads, from node:worker_threads, run separate JavaScript environments inside the same process, each with its own loop and heap. They communicate by message passing, and they can share raw memory through SharedArrayBuffer when a copy is too expensive. This is the right tool for CPU bound tasks inside an otherwise responsive Node JS server, ideally with a small pool of long lived workers rather than one thread per job.
The cluster module and plain child processes take the other route: several operating system processes, each isolated, with incoming connections distributed among them. Isolation is the advantage, since one crashed worker does not take the others down, and the cost is that nothing is shared except through sockets or external stores. Many production Node JS deployments run one process per core this way and keep the code inside each process blissfully single threaded.
The third answer is to scale outward. Because a Node JS process is small and starts quickly, running many replicas behind a load balancer is often simpler than orchestrating threads, and it matches how containers and serverless platforms already work. Whichever route you take, keep session state out of process memory so that any instance can serve any request.
[07]Release lines and versions
Node JS ships a new major version every six months, in April and in October. Even numbered majors are the ones that matter for most users: after six months as the Current line, an even major becomes a long term support release in October of the year it appeared. Odd numbered majors never become LTS and exist to give experimental work a real audience before it lands somewhere permanent.
Once a line enters long term support it gets twelve months as Active LTS, where backports are generous, and then a further eighteen months of maintenance limited to serious bug fixes, security patches, and documentation. That gives thirty months of support from the LTS date, or three years from the original release. The table below traces the recent even numbered lines of Node JS.
| Line | First release | Entered LTS | End of life |
|---|---|---|---|
| 18 | April 2022 | October 2022 | April 2025 |
| 20 | April 2023 | October 2023 | April 2026 |
| 22 | April 2024 | October 2024 | April 2027 |
| 24 | May 2025 | October 2025 | April 2028 |
The practical guidance is short. Run the most recent Active LTS line in production, keep patch updates flowing continuously, and plan the jump to the next LTS line while the current one is still in maintenance rather than after it expires. Try the Current line in development if a new capability interests you, but do not let an unsupported Node JS version become the foundation of a service you have to keep alive.
[08]Support cycle of an even numbered line
The chart shows how one even numbered major moves through its phases, measured in months from its first release. The proportions are what matter when you plan an upgrade: the loud part of a version's life is short, and most of its supported existence is quiet maintenance.
- >Current, 6 months: new features land, semver major changes are still possible in the next line.
- >Active LTS, 12 months: features are backported when they are safe; recommended for production.
- >Maintenance, 18 months: critical bugs and security fixes only, then end of life.
Read the figure as a planning tool. If you adopt a Node JS line when it becomes Active LTS, you have around two and a half years before it stops receiving fixes, and the next LTS line will have been available for eighteen of those months. Teams that treat the runtime upgrade as an annual chore rather than an emergency spend far less time on it.
[09]Node JS and the alternatives
Server side JavaScript is no longer a single runtime. Deno, from the original author of Node JS, was released in 2020 with permissions off by default and TypeScript support built in. Bun arrived at version 1.0 in 2023 on a different engine, JavaScriptCore, with speed and an all in one toolchain as its pitch. Both are compatible with much of the npm ecosystem, and both borrow ideas that Node JS has since absorbed in its own way.
| Criterion | Node JS | Deno | Thread per request stacks |
|---|---|---|---|
| First release | 2009 | 2018, 1.0 in 2020 | Varies by platform |
| Engine | V8 | V8 | JVM, CLR, CPython and others |
| Concurrency model | Event loop, thread pool, workers | Event loop, workers | Threads, often pooled |
| Modules | CommonJS and ESM | ESM first | Language specific |
| Ecosystem depth | Very large, npm registry | Own registry plus npm compatibility | Mature, language specific |
| Hosting support | Near universal | Growing | Near universal |
Against thread per request platforms such as typical JVM or Python setups, the trade is legibility for isolation. Those stacks let a developer write straight line blocking code and let the scheduler sort out concurrency, at the cost of memory per thread and synchronization bugs. Node JS makes concurrency explicit in the syntax, which suits I/O heavy services and punishes accidental computation on the hot path.
For most teams the deciding factors are not benchmarks. They are the size of the ecosystem, the number of engineers who already know the language, the availability of hosting and observability tooling, and the length of the support window. On all four, Node JS remains the conservative choice, and the newer runtimes are worth watching and worth prototyping with.
[10]Where Node JS fits and where it does not
The runtime is at its best when a program's job is to move data between other systems. Teams at large consumer platforms including Netflix, LinkedIn, and PayPal have published accounts of running Node JS in front of their existing services, and the pattern is consistent: a thin, fast layer that aggregates and shapes responses rather than performing heavy computation itself.
- >HTTP and GraphQL APIs, gateways, and backend for frontend layers that fan out to internal services.
- >Real time features over WebSockets or server sent events, including chat, presence, and live dashboards.
- >Server rendered and hybrid web applications, where the same language runs on both sides of the wire.
- >Command line tools, code generators, and automation scripts distributed through the registry.
- >Streaming pipelines and queue consumers that transform records as they pass through.
Front end tooling deserves its own mention, because it is where almost every JavaScript developer meets the runtime first. Bundlers, linters, formatters, type checkers, test runners, and package scripts all execute on Node JS, which means even a team writing exclusively browser code depends on the version installed on their machines and in CI.
Real time work suits the model particularly well. Thousands of open sockets with occasional small messages is exactly the shape of load an event loop handles cheaply, and holding a connection open in Node JS costs a handle and a little memory rather than a thread. The constraint shows up elsewhere, in coordinating state across replicas, which is a distributed systems problem rather than a runtime one.
The weak spots are predictable. Sustained numeric computation, large scale image or video processing, and workloads needing precise memory control fit poorly, since a garbage collected single loop is the wrong instrument. Long batch jobs that saturate a core are better off in a dedicated worker or another language. Node JS can call out to native addons or spawn a specialized binary, which is often the pragmatic middle path.
None of this makes the choice binary. Plenty of healthy architectures put Node JS at the edge, where request shaping and streaming happen, and keep the compute intensive core in whatever language suits it, with a queue or an RPC boundary between them.
[11]Running Node JS in production
Getting code to work locally is the easy half. A Node JS service in production needs a supervisor that restarts it, a graceful shutdown path that stops accepting connections and drains the in flight ones before exiting, and a health endpoint that tells the truth about downstream dependencies rather than simply returning a success code.
Observability starts with structured logs written to standard output and collected by the platform, one JSON object per event, correlated by a request identifier. Add metrics for event loop delay, heap usage, active handles, and request latency percentiles, since loop delay is the earliest signal that something synchronous has crept into a hot path. The built in inspector, heap snapshots, and CPU profiles are the tools for the investigation that follows.
Configuration belongs in the environment, not in the image. Read it once at startup, validate it, and fail loudly on anything missing rather than discovering the gap on the first request. Recent Node JS versions can load an environment file directly with a command line flag, which removes one small dependency from most projects.
For packaging, a small base image, a production only install driven by the lockfile, a non root user, and a pinned major version give reproducible deployments. Keep the Node JS version in your container, your CI configuration, and your local tooling aligned, because a mismatch between them produces the most tedious category of bug there is: code that works on one machine and not another for no visible reason.
Performance tuning, when it becomes necessary, follows a boring order. Profile first, then remove unnecessary work such as redundant serialization or repeated parsing, then add caching where the data allows it, then move computation off the loop, and only then reach for more instances. Most Node JS services that feel slow are waiting on a database query or an upstream call, and no runtime setting fixes that.
[12]Security practices
Security in Node JS splits into two halves: the code you wrote and the code you installed. Your own half is the familiar list, namely validating input at the boundary, using parameterized queries, escaping output, keeping secrets in the environment, setting sensible request size and timeout limits, and never passing user input into a shell command.
The installed half is where the ecosystem's openness cuts both ways. Audit dependencies regularly, pin them with a lockfile, review the diff when a package updates itself in unexpected ways, prefer packages with narrow scope and active maintenance, and be wary of lifecycle scripts that run on install. A compromised transitive dependency has the same access to your process as your own modules do.
Node JS has also grown a permission model that restricts what a process may do at the runtime level, limiting file system reads and writes, child process creation, and native addon loading by command line flag. Combined with running as an unprivileged user and with a read only file system where possible, it narrows the damage a hostile package or an injection bug can cause.
Finally, patch promptly. The project publishes security releases across all supported lines, usually with an advance notice, and staying on a supported Node JS version is what makes those fixes available to you at all. An end of life runtime accumulates unpatched vulnerabilities silently, which is the most common way an otherwise careful deployment ends up exposed.
[13]How to get started
A first working setup takes minutes. The sequence below is the one experienced developers follow when they install Node JS on a new machine, and it is worth following in order because step one prevents most of the problems that would otherwise appear later.
- 01 Install through a version manager rather than a single system wide package. Managers let you switch the active Node JS version per project, which matters as soon as you work on more than one codebase, and they keep the runtime out of directories that need administrator rights.
-
02
Select the current Active LTS line and record it. Put the version in a file the manager reads and in your CI configuration so every environment agrees. Verify with
node --versionand check that the bundled package client responds too. -
03
Initialize a project, set
"type": "module"so you write standard imports, and create a single entry file. Start an HTTP server with the core module alone, no dependencies, to confirm the Node JS installation works end to end before any framework enters the picture. - 04 Add the development loop. Run the entry file with the watch flag so changes restart the process, write a first test with the built in runner, and add scripts to the manifest so collaborators do not need to remember commands.
- 05 Only then add dependencies, one at a time and with a reason. Commit the lockfile, and read the release notes for your Node JS line occasionally, since features you were about to install may already be part of the runtime.
From that base, the natural next steps are structured logging, a configuration module that validates the environment at startup, and a deployment target. Everything after that is application design rather than Node JS setup.
[14]Frequently asked questions
Is Node JS a language or a framework?
Neither. Node JS is a runtime: a program that executes JavaScript along with a standard library for files, networking, and processes. The language is JavaScript, standardized as ECMAScript, and frameworks such as the popular web libraries are packages you install on top of the runtime.
Does it cost anything?
No. Node JS is free and open source under the MIT license, for personal and commercial use alike, with no registration or licensing tier. The public package registry is also free to use for publishing and installing public packages.
Which version should a new project use?
The most recent Active LTS line, which is always an even numbered major. It has the longest remaining support window and the widest compatibility across hosting providers and packages. Use the Current line for experiments, and avoid any line that has reached end of life.
Is it really single threaded?
Your JavaScript runs on one thread per process, so two of your functions never execute at the same instant. The process itself is multithreaded: libuv keeps a pool for file, DNS, compression, and some cryptographic work, and you can start additional JavaScript threads with the worker threads module when computation needs its own lane.
Can it run TypeScript directly?
Increasingly, yes. Node JS gained the ability to strip type annotations and execute the remaining JavaScript, arriving experimentally in the 22 line and becoming easier to use in later releases. Stripping is not type checking, so keep running the TypeScript compiler in your build or editor for that.
Should I use CommonJS or ESM?
For new code, ESM, since it is the standard and is fully supported. CommonJS remains a first class citizen because a vast amount of published code uses it, and current Node JS versions make mixing the two far less painful than it once was. What matters most is being explicit about which format a package uses.
How does it relate to npm?
They are separate projects that ship together. Installing Node JS gives you the npm client and the npx runner, which talk to the public registry. You can swap the client for pnpm or Yarn without changing the runtime, since all of them resolve the same manifests and registry.
Is it a good fit for CPU heavy work?
Not on the main thread of a service that also answers requests. Move the computation into a worker thread or a separate process, or delegate it to a native addon or a specialized tool. Node JS can coordinate that work well; it simply should not perform long calculations where the event loop is trying to turn.
Most questions about Node JS performance turn out to be questions about what else is holding the loop.