% node-runtime.tex — Node.js internals for seniors: V8 + libuv + bindings, OS async I/O vs
% the libuv thread pool, blocking the loop, streams + backpressure, Buffer, EventEmitter,
% worker_threads vs cluster vs child_process, crash semantics, graceful shutdown, ESM,
% and Node vs Deno vs Bun. Source: own knowledge (no repo sources). Versions only where sure.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/runtime/node-runtime.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=runtime kind=concept level=senior platform=backend new=no round=typescript-2026-09-24 topic=concurrency,performance
% @tags: libuv, thread-pool, uv-threadpool-size, epoll, blocking-event-loop, streams, backpressure, buffer, eventemitter, worker-threads, cluster, graceful-shutdown
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{TSSheet}{
  morekeywords={import,from,export,default,const,let,type,interface,extends,function,
    return,async,await,if,else,switch,case,new,true,false,null,undefined,typeof,keyof,
    as,satisfies,never,string,number,boolean,void,declare,namespace,readonly,for,of},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/},
  morestring=[b]", morestring=[b]', morestring=[b]`}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  os/.style={sb, draw=sheetGreen!70!black, fill=sheetGreen!10},
  tp/.style={sb, draw=sheetOrange, fill=sheetOrange!10},
  bad/.style={sb, draw=sheetRed, fill=sheetRed!7},
  bc/.style={draw=sheetGrey, fill=sheetBlue!15, minimum width=3mm, minimum height=2.2mm, inner sep=0pt},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  pt/.style={font=\bfseries\small, anchor=west},
}

\begin{document}

\sheettitle{Node.js runtime — libuv, streams, scaling, shutdown}{runtime · memo}

\oneliner{Node = \textbf{V8} (runs JS) + \textbf{libuv} (event loop, OS async I/O, a
\textbf{4-thread pool}) + C++ \textbf{bindings}. Network I/O needs \emph{no} threads (epoll /
kqueue readiness); fs, \texttt{dns.lookup}, crypto and zlib go to the pool. Your JS stays on
\textbf{one thread} — block it and \emph{every} request waits.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \draw[sheetGrey!40] (9.55,3.0) -- (9.55,-0.9);
  \node[pt] at (0,2.9) {\textcolor{sheetBlue}{1} where the work happens};
  \node[sb, minimum width=38mm] (js) at (2.2,2.3) {your JS + \texttt{node:*} modules (JS)};
  \node[sb, minimum width=17mm] (v8) at (1.1,1.45) {\textbf{V8}\\run JS · GC};
  \node[sb, minimum width=17mm] (bi) at (3.3,1.45) {C++ bindings\\(\texttt{fs}, \texttt{net}, …)};
  \node[sb, minimum width=24mm, fill=sheetBlue!15] (lp) at (2.2,0.5) {\textbf{libuv event loop}\\the ONE JS thread};
  \node[os, minimum width=40mm] (k) at (2.2,-0.55) {kernel: \textbf{epoll} (Linux) / \textbf{kqueue} (macOS)\\sockets · pipes · signals — readiness, no threads};
  \draw[flow] (js) -- (v8); \draw[flow] (js) -- (bi); \draw[flow] (bi) -- (lp);
  \draw[flow, <->] (lp) -- node[lbl, right]{ready?} (k);
  \node[tp, minimum width=33mm] (pool) at (7.3,1.55) {\textbf{thread pool} (\texttt{UV\_THREADPOOL\_SIZE}=4)};
  \foreach \i/\x in {1/6.15,2/6.9,3/7.65,4/8.4} \node[tp, minimum width=6mm, font=\tiny] (w\i) at (\x,0.95) {T\i};
  \node[lbl, text=sheetOrange!80!black] at (7.3,2.25) {fs.* · \texttt{dns.lookup} (getaddrinfo)\\crypto pbkdf2/scrypt/randomBytes · zlib async};
  \draw[hot] (lp.east) -- (4.9,0.5) |- (pool.west);
  \node[lbl, text=sheetOrange!80!black, anchor=west] at (4.95,1.05) {job};
  \draw[flow, dashed] (w4.south) |- (lp.south east);
  \node[lbl] at (6.6,0.12) {done $\to$ callback queued on the loop};
  \node[lbl, text=sheetRed] at (7.3,-0.5) {a 5th job queues: slow \texttt{dns.lookup}s stall \texttt{fs}};
  % ── streams ──
  \node[pt] at (9.65,2.9) {\textcolor{sheetBlue}{2} streams: backpressure};
  \node[sb, minimum width=15mm] (r) at (10.6,1.9) {Readable\\file};
  \node[sb, minimum width=15mm] (t) at (13.0,1.9) {Transform\\gzip};
  \node[sb, minimum width=15mm] (w) at (15.4,1.9) {Writable\\socket};
  \draw[flow] (r) -- (t); \draw[flow] (t) -- (w);
  \foreach \k in {0,1,2,3} \node[bc] at (14.8+\k*0.32,1.2) {};
  \foreach \k in {0,1,2} \node[bc] at (12.4+\k*0.32,1.2) {};
  \draw[sheetRed, thick] (15.9,1.0) -- (15.9,1.4);
  \node[lbl, text=sheetRed, anchor=west] at (15.95,1.2) {hwm};
  \node[lbl] at (13.7,1.2) {buffers};
  \draw[hot, sheetRed] (14.55,0.55) -- node[lbl, below, text=sheetRed]{\texttt{write()} returns \texttt{false} $\to$ pause upstream} (10.6,0.55) -- (r.south);
  \draw[flow, sheetGreen!60!black] (15.4,0.05) -- node[lbl, below, text=sheetGreen!50!black]{\texttt{'drain'} $\to$ resume} (10.9,0.05) -- ++(0,0.4);
  \node[lbl, anchor=west] at (9.65,-0.6) {memory stays $\approx$ hwm per stage, whatever the file size};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}\raggedright
  \item \textbf{OS async} for sockets: the kernel reports ready fds (poll phase); 10k idle
        connections cost memory, not threads.
  \item \textbf{Thread pool} for what the OS cannot do asynchronously (regular files,
        \texttt{getaddrinfo}) or is CPU-heavy (crypto, zlib). Default \textbf{4}, max 1024;
        set \texttt{UV\_THREADPOOL\_SIZE} before first use. \texttt{dns.resolve*} uses c-ares
        on the network, not the pool.
  \item \textbf{Blocking the loop}: \texttt{*Sync} APIs, \texttt{JSON.parse}/\texttt{stringify}
        of MBs, ReDoS regex, big sorts. Symptoms: p99 jumps on \emph{all} routes, timers late,
        health checks time out, \emph{one} core at 100\%. Diagnose:
        \texttt{perf\_hooks.monitorEventLoopDelay()}, \texttt{--cpu-prof} flame graph. Fix:
        stream, chunk + yield, or a worker.
  \item \textbf{Streams}: Readable, Writable, Duplex (socket), Transform (zlib). Buffer per
        stream = \texttt{highWaterMark}: 64 KiB for bytes (16 KiB before Node 22), 16 objects
        in objectMode. \texttt{pipeline()} wires backpressure \emph{and} error + destroy of
        every stage; \texttt{.pipe()} forwards no errors (leaks fds). Readables are
        \texttt{for await}-able.
  \item \textbf{Buffer}: a \texttt{Uint8Array} subclass, memory outside the V8 heap
        (\texttt{memoryUsage().external}). \texttt{alloc} is zeroed; \texttt{allocUnsafe} may
        hold old bytes. Encodings: utf8, hex, base64(url), latin1, utf16le. A UTF-8 char can
        split across chunks: \texttt{setEncoding('utf8')}, not \texttt{chunk.toString()}.
  \item \textbf{EventEmitter}: \texttt{emit} is \textbf{synchronous}, in registration order;
        \texttt{'error'} with no listener \textbf{throws}; $>$10 listeners for one event
        $\to$ \texttt{MaxListenersExceededWarning} (usually a leak).
\end{itemize}

\section{Example — stream, iterate, shut down}
\begin{lstlisting}[language=TSSheet]
import { pipeline } from 'node:stream/promises';
await pipeline(createReadStream('big.log'), createGzip(),
  createWriteStream('big.log.gz'));  // bounded memory + cleanup
for await (const chunk of req) size += chunk.length;
process.on('SIGTERM', () => {        // k8s / systemd stop
  server.close(async () => {         // no new conns, finish old
    await db.end(); process.exit(0); });
  setTimeout(() => process.exit(1), 10_000).unref(); // deadline
});
\end{lstlisting}

\section{Scaling past one thread}
{\scriptsize\renewcommand{\arraystretch}{1.1}\setlength{\tabcolsep}{3pt}
\begin{tabular}{@{}>{\raggedright}p{15mm}>{\raggedright}p{27mm}>{\raggedright}p{19mm}>{\raggedright\arraybackslash}p{13mm}@{}}
\toprule
 & isolation / sharing & talk & for \\
\midrule
\texttt{worker\_}\allowbreak\texttt{threads} & own V8 isolate + loop, same process; \texttt{SharedArrayBuffer} + \texttt{Atomics} & \texttt{postMessage} (clone), transfer \texttt{ArrayBuffer} & CPU work (pool them) \\
\texttt{cluster} & N processes, no shared memory; primary shares the listen port (round-robin) & IPC messages & use all cores for a server \\
\texttt{child\_}\allowbreak\texttt{process} & any program; \texttt{spawn} streams, \texttt{exec} = shell + buffer, \texttt{fork} = Node + IPC & stdio / IPC & tools, isolation \\
\bottomrule
\end{tabular}}

\section{Errors, crashes, modules}
\begin{itemize}\raggedright
  \item \textbf{\texttt{unhandledRejection}} crashes the process by default since
        \textbf{Node 15} (\texttt{--unhandled-rejections=throw}). \textbf{\texttt{uncaughtException}}:
        log and exit — state is unknown; let the supervisor restart.
  \item \textbf{ESM}: \texttt{"type":"module"} makes \texttt{.js} ESM; \texttt{.mjs} is always
        ESM, \texttt{.cjs} always CJS. ESM: full file extensions, top-level \texttt{await},
        no \texttt{\_\_dirname} (\texttt{import.meta.url}). \texttt{require()} of sync ESM:
        unflagged in Node 22.12.
\end{itemize}

\section{Node vs Deno vs Bun}
{\scriptsize\renewcommand{\arraystretch}{1.1}\setlength{\tabcolsep}{3pt}
\begin{tabular}{@{}>{\raggedright}p{11mm}>{\raggedright}p{18mm}>{\raggedright}p{20mm}>{\raggedright\arraybackslash}p{19mm}@{}}
\toprule
 & Node & Deno & Bun \\
\midrule
engine & V8 & V8 & JavaScriptCore \\
core in & C++ + libuv & Rust + Tokio & Zig \\
TS & strips types (23.6+) & native & native \\
security & all access; opt-in \texttt{--permission} & \textbf{deny by default}, \texttt{--allow-net} … & all access \\
npm & native & \texttt{npm:} specifiers & npm-compatible \\
\bottomrule
\end{tabular}}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{``Node is single-threaded'' — \emph{your JS} is; libuv, V8's GC and the pool are not.}
  \trap{Raising the pool size does not fix CPU-bound JS on the loop — only a worker does.}
  \trap{\texttt{emit()} is not async: a slow listener blocks the emitter's caller.}
  \trap{Cluster/processes share nothing: sessions and rate limits need Redis/DB.}
\end{itemize}

\section{Remember}
\emph{Kernel for sockets, pool for files, workers for CPU — and never block the one loop.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item Why is one slow endpoint slowing all? — sync CPU on the loop; profile, move to a worker.
  \item Backpressure? — \texttt{write()} false $\to$ stop reading until \texttt{'drain'}; \texttt{pipeline} does it.
  \item iOS analogue? — loop $\approx$ main thread, pool $\approx$ a 4-wide GCD queue, workers $\approx$ isolated actors.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} js-event-loop · js-engines-performance ·
rn-architecture · GCD / Swift concurrency}

\end{document}
