The os Module

The os module is one of Node.js’s built-in modules that gives your program a window into the machine it is running on: how many CPU cores it has, how much memory is free, what operating system and architecture it’s running on, who the current user is, and where to put temporary files. It requires no installation — it ships with Node itself — and almost every call it exposes is synchronous, because the information it returns is cheap to read from the operating system.

Overview / How It Works

Under the hood, os is a thin JavaScript wrapper around system calls that libuv (Node’s C library for async I/O and OS abstraction) exposes in a cross-platform way. When you call os.cpus() on Linux, Node reads from /proc/cpuinfo-equivalent kernel interfaces; on Windows and macOS it calls into the native APIs. You never see any of that — you get back the same shaped JavaScript object no matter which OS you’re on, which is the entire point of the module: write one program, ask "how much memory do I have?", and get a sensible answer everywhere.

Because these calls are synchronous and typically resolve in microseconds (they’re reading cached kernel state, not touching disk or network), calling os.freemem() or os.cpus() does not block the event loop in any meaningful way — unlike fs.readFileSync() on a large file, there’s no noticeable I/O wait here. That’s why the entire os API is synchronous with no promise-based or callback-based equivalents; there was never a need for one.

A second thing to understand is that most values os reports describe the host machine, not necessarily the sandbox your process is confined to. Inside a Docker container with a memory limit, os.totalmem() still reports the physical RAM of the host node, not the container’s cgroup limit. This trips people up constantly and is covered in Common Mistakes below.

Syntax

Import the module, then call whichever properties or methods you need. Import with the node: prefix so it’s unambiguous that this is a built-in, not a package from npm.

const os = require("node:os");

os.platform();      // "linux", "darwin", "win32", ...
os.arch();           // "x64", "arm64", ...
os.cpus();            // array of core info objects
os.totalmem();        // total RAM in bytes
os.freemem();          // free RAM in bytes
os.homedir();          // current user's home directory
os.tmpdir();           // OS temp directory
os.hostname();         // machine hostname
os.uptime();            // system uptime in seconds
os.loadavg();           // [1m, 5m, 15m] load averages (POSIX only)
os.EOL;                  // "\n" on POSIX, "\r\n" on Windows
Member Returns Notes
os.platform() string e.g. linux, darwin, win32
os.arch() string e.g. x64, arm64
os.type() string e.g. Linux, Darwin, Windows_NT
os.release() string kernel/OS release version
os.cpus() array one entry per logical core: model, speed, times
os.totalmem() / os.freemem() number (bytes) host memory, not container limit
os.loadavg() [num, num, num] always [0,0,0] on Windows
os.homedir() string current user’s home directory
os.tmpdir() string default directory for temp files
os.hostname() string machine’s network hostname
os.userInfo() object username, uid, gid, homedir, shell
os.networkInterfaces() object network adapters and their addresses
os.EOL string native line-ending for the platform

Examples

Example 1: A system information report

const os = require("node:os");

console.log("Platform:", os.platform());
console.log("Architecture:", os.arch());
console.log("OS Release:", os.release());
console.log("Hostname:", os.hostname());
console.log("Home directory:", os.homedir());
console.log("Temp directory:", os.tmpdir());
console.log("Uptime (seconds):", os.uptime());
console.log("Total memory (MB):", Math.round(os.totalmem() / 1024 / 1024));
console.log("Free memory (MB):", Math.round(os.freemem() / 1024 / 1024));
Platform: linux
Architecture: x64
OS Release: 6.8.0-136-generic
Hostname: my-server
Home directory: /root
Temp directory: /tmp
Uptime (seconds): 154302
Total memory (MB): 16034
Free memory (MB): 5122

Every value here reflects the actual machine running the script, so your own output will differ. This kind of report is genuinely useful at the top of a CLI tool or health-check endpoint, so you (or a user filing a bug) can see exactly what environment the code ran in.

Example 2: CPU details and load average

const os = require("node:os");

const cpus = os.cpus();
console.log(`This machine has ${cpus.length} logical CPU cores.`);
console.log("First core model:", cpus[0].model);
console.log("First core speed (MHz):", cpus[0].speed);

const idleTotal = cpus.reduce((sum, cpu) => sum + cpu.times.idle, 0);
const allTotal = cpus.reduce((sum, cpu) => {
  const t = cpu.times;
  return sum + t.user + t.nice + t.sys + t.idle + t.irq;
}, 0);
const idlePercent = ((idleTotal / allTotal) * 100).toFixed(1);
console.log(`Cores idle: ${idlePercent}%`);

if (os.platform() !== "win32") {
  const [load1, load5, load15] = os.loadavg();
  console.log(`Load average: 1m=${load1.toFixed(2)} 5m=${load5.toFixed(2)} 15m=${load15.toFixed(2)}`);
} else {
  console.log("os.loadavg() always returns [0, 0, 0] on Windows.");
}
This machine has 8 logical CPU cores.
First core model: Intel(R) Xeon(R) CPU @ 2.20GHz
First core speed (MHz): 2200
Cores idle: 91.3%
Load average: 1m=0.42 5m=0.31 15m=0.28

os.cpus() returns one entry per logical core, each with a times object giving cumulative CPU-time-since-boot in each of the kernel’s accounting buckets (user, nice, sys, idle, irq). Comparing two snapshots taken a second apart, rather than a single snapshot, is how tools like top compute a live percentage — a single read only tells you totals since boot. os.loadavg() mirrors the Unix "load average" you’d see from the uptime command; Windows has no equivalent concept, so Node always reports zeros there.

Example 3: A preflight check before heavy work

const os = require("node:os");
const fs = require("node:fs/promises");
const path = require("node:path");

async function preflightCheck() {
  const freeRatio = os.freemem() / os.totalmem();
  if (freeRatio < 0.1) {
    throw new Error(`Only ${(freeRatio * 100).toFixed(1)}% memory free \u2014 aborting heavy job.`);
  }

  const logLines = [
    `Host: ${os.hostname()}`,
    `Platform: ${os.platform()} (${os.arch()})`,
    `CPUs: ${os.cpus().length}`,
    `Free memory: ${Math.round(os.freemem() / 1024 / 1024)} MB`,
  ];

  const logPath = path.join(os.tmpdir(), "preflight.log");
  await fs.writeFile(logPath, logLines.join(os.EOL) + os.EOL);
  console.log("Preflight log written to:", logPath);
}

preflightCheck().catch((err) => {
  console.error("Preflight failed:", err.message);
  process.exitCode = 1;
});
Preflight log written to: /tmp/preflight.log

This combines several os members into something realistic: refuse to start a memory-heavy job if less than 10% RAM is free, then write a small diagnostic log using os.tmpdir() for a safe, writable location and os.EOL so the file’s line endings match the host OS’s convention. Notice path.join() is used to combine the directory and filename instead of string concatenation — more on why that matters below.

How It Works Step by Step

  • When your script calls an os function, Node makes a direct, blocking call into libuv’s OS-abstraction layer — there is no callback, no promise, no event loop tick involved, because these are not I/O operations in the async sense.
  • The call returns immediately with a plain JavaScript value or object; nothing is queued or deferred, so os.freemem() called twice in a row on the very next line can (and will) return two different, up-to-the-moment numbers.
  • Values like os.cpus()[i].times are cumulative counters since the machine booted, not since your process started — that’s why computing a percentage requires two snapshots and a delta, as shown in Example 2.
  • Nothing here is cached by Node between calls; every call re-reads live system state, so polling os.loadavg() in a monitoring loop is cheap and gives fresh data each time.

Common Mistakes

Mistake 1: Trusting os.totalmem() / os.freemem() as container limits

Inside a Docker container capped at 512 MB, os.totalmem() still reports the host machine’s full RAM (say, 16 GB), because the call reads host-level kernel state, not the container’s cgroup memory limit. Code that throttles work based on these values will badly misjudge how much memory is actually available.

// Wrong: assumes os.freemem() reflects this container's actual budget
if (os.freemem() > 500 * 1024 * 1024) {
  startHeavyJob();
}

In a container that has 16 GB of host RAM but is capped at 512 MB, this check passes even though the process is one allocation away from being OOM-killed. Prefer an explicit, container-aware limit instead of trusting os for this:

// Better: use an explicit budget the deployment environment controls
const memoryLimitMb = Number(process.env.MEMORY_LIMIT_MB ?? 512);
const usedMb = process.memoryUsage().rss / 1024 / 1024;
if (memoryLimitMb - usedMb > 100) {
  startHeavyJob();
}

Mistake 2: String-concatenating paths from os.homedir() / os.tmpdir()

Paths returned by os.homedir() and os.tmpdir() use the host OS’s native separator — forward slash on POSIX, backslash on Windows. Concatenating a hardcoded "/" onto them works by accident on Linux/macOS and produces a broken path on Windows.

// Wrong: hardcodes a POSIX separator
const configPath = os.homedir() + "/.myapp/config.json";
// Correct: let path.join() pick the right separator for the host OS
const path = require("node:path");
const configPath = path.join(os.homedir(), ".myapp", "config.json");

Always build paths with node:path‘s join() or resolve() rather than template strings, precisely so code that touches os.homedir() or os.tmpdir() behaves correctly on every platform without an if (os.platform() === "win32") branch.

Best Practices

  • Import with the node: prefix (require("node:os")) to make it unambiguous this is a core module.
  • Never treat os.totalmem() / os.freemem() as an authoritative container or cgroup budget — use an explicit, environment-provided limit for anything running in Docker, Kubernetes, or similar.
  • Always combine os.homedir(), os.tmpdir(), or any other os-returned path with path.join() / path.resolve(), never manual string concatenation.
  • Use os.EOL when writing plain-text files line by line so they open cleanly in native editors on every OS.
  • Remember os.loadavg() is meaningless on Windows — branch on os.platform() before relying on it, or use os.cpus() deltas for a cross-platform CPU-usage estimate instead.
  • Treat os.cpus().length as a sizing hint for worker pools (e.g. with worker_threads or cluster), not as a hard cap — a container’s CPU quota can be lower than the host’s core count.

Practice Exercises

  • Write a script that prints a one-line health summary: hostname, platform, number of CPU cores, and free memory as a percentage of total memory.
  • Write a function that takes two snapshots of os.cpus() one second apart (using a delay) and computes the overall CPU busy percentage across all cores, rather than reading a single snapshot.
  • Write a script that writes a small report file into os.tmpdir() using path.join() and os.EOL, then reads it back and prints its contents to confirm the line endings match what you expect on your OS.

Summary

  • The os module is a built-in, synchronous API for reading information about the host machine: CPU, memory, platform, users, and paths.
  • Its calls are fast and synchronous because they read cheap, cached kernel state — there is no async version because none is needed.
  • os.totalmem() / os.freemem() report the physical host’s memory, not a container’s cgroup limit — don’t use them as a container memory budget.
  • Always join paths returned by os.homedir() / os.tmpdir() with path.join() instead of string concatenation, so code stays correct on Windows.
  • os.loadavg() is POSIX-only and always [0, 0, 0] on Windows; use os.cpus() time deltas for a cross-platform CPU metric.