Production Readiness

By Syncfusion®, Inc. · Last updated

What NODE_ENV changes in Node.js and libraries, validating configuration at start-up, process managers and restart strategies, one process per container, running as PID 1, health check endpoints, graceful shutdown with signals, server.close and closeIdleConnections, finishing in-flight work and closing resources and workers, shutdown timeouts, --max-http-header-size and the compile cache, structured JSON logging and log levels, diagnostics_channel, safe Node.js upgrades with a CI version matrix and deprecation flags, single executable applications and bundling, and a pre-production checklist.

Production Readiness

Production = the place where real users use your app, as opposed to your own computer.

  • In production, the app must start correctly, stay up and stop cleanly.
  • It must tell you what is happening, through logs and health checks.
  • Small habits make the difference: check settings, handle signals, log well.

Simple lesson: production-ready means it starts safely, runs steadily and stops cleanly.

A Real-World Example

Think of the school bus before the morning trip.

  • The driver checks fuel, tyres and brakes before starting. That is config validation.
  • The transport office calls every hour: "All fine?" That is a health check.
  • At the last stop, the driver waits for every child to get off before locking the bus. That is graceful shutdown.
  • The driver fills in the logbook at every stop. That is logging.

What You Will Learn

In this lesson, you will learn:

  1. What NODE_ENV changes, and how to validate settings
  2. Process managers, containers and PID 1
  3. Health check endpoints
  4. Graceful shutdown, step by step
  5. Shutdown timeouts and forced exit
  6. Tuning flags and the compile cache
  7. Structured JSON logs, log levels and diagnostics_channel
  8. Upgrading Node.js safely
  9. Single executable apps, bundling, and a pre-production checklist

Runtime Configuration

NODE_ENV Behavior in Node.js and Libraries

NODE_ENV = an environment variable that says what mode the app runs in, usually production or development.

  • Node.js itself ignores it. It does not make Node.js faster on its own.
  • Libraries read it. Express, for example, caches views and hides error details when it is production.
  • Use real settings for real choices. Read LOG_LEVEL or CACHE_SECONDS, rather than checking NODE_ENV everywhere.

Validating Environment Configuration

  • Read every setting once, at start-up, into one config object.
  • Check that required settings exist and have the right type.
  • If something is wrong, print a clear message and exit with code 1. This is fail fast.
  • A crash at start-up is much better than a strange error at 2 AM.

Print the settings you loaded at start-up, except secrets. It makes support much easier.

  • Delete the DB_URL line from .env and press ▶ Run. The app refuses to start, with a clear reason.

Simple lesson: read and check all settings at start-up, and stop at once if one is wrong.

Settings come from defaults, then the .env file, then real environment variables. The last one wins defaults in codePORT = 3000 .env filePORT = 4000 real env variablePORT = 8080 configport: 8080

Check the config once at start-up, and stop with a clear message if it is wrong.

Later sources win. Real environment variables beat the .env file, and both beat defaults.

Running Reliably

Process Managers and Restart Strategies

Process manager = a program that starts your app, watches it, and restarts it if it crashes.

  • systemd on Linux servers, PM2 for Node.js apps, or the container platform itself.
  • Restart strategy = restart after a crash, but wait a little longer each time (back-off), so a broken app does not restart thousands of times a minute.
  • This is why fail fast works: crash cleanly, and the manager brings back a fresh copy.
npm install -g pm2
pm2 start app.js --name school-api   # start and watch
pm2 logs school-api                  # read the logs
pm2 restart school-api               # restart after a deploy

One Process per Container

  • In Docker and Kubernetes, run one Node.js process per container.
  • To use more cores, run more containers. The platform's load balancer shares the traffic.
  • Run node app.js directly, not npm start, so signals reach Node.js.

Kubernetes sends SIGTERM, waits about 30 seconds, then sends SIGKILL. Finish shutting down within that time.

Running as PID 1 in Containers

  • In a container, the first process gets PID 1. Linux treats PID 1 specially.
  • PID 1 gets no default signal handling. Without your own SIGTERM handler, docker stop waits and then kills it hard.
  • Fix it: handle SIGTERM yourself, or start with docker run --init (or tini), which forwards signals for you.
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

Health Check Endpoints

Health check = a small URL the platform calls to ask "are you alive and ready?"

  • Liveness = the process is running. If this fails, restart it.
  • Readiness = the app can serve users (database connected). If this fails, send no traffic yet.
  • Keep it fast and cheap. Return 200 when healthy, 503 when not.
  • Right after start, /health/ready says starting with status 503, because the pretend database is not connected yet.
  • Send it again a moment later. Now it says ready with status 200.

Simple lesson: a manager restarts you, a health check tells it when, and PID 1 must handle signals.

Graceful Shutdown

Graceful shutdown = stop taking new work, finish the work in progress, close everything, then exit.

Why Shutdown Must Be Graceful

  • Every deploy and every scale-down stops your app.
  • If it stops in the middle of a request, a user sees an error, or a payment is half saved.
  • A graceful stop takes a few seconds and loses nothing.

Animation: on SIGTERM the server stops new connections, waits for in-flight requests, closes the database and exits, with a timeout as a safety net

SIGTERM server.close()no new users finish workin-flight requests closeDB, files, workers exit(0)

Safety net: if this takes over 10 seconds, force exit(1)

Stop new work, finish old work, close resources, exit. A timer forces the exit if something hangs.

Steps

  1. Wire shutdown to signals: process.on("SIGTERM", shutdown) and SIGINT.
  2. Stop new connections: server.close(). Requests already started keep going.
  3. Close idle connections: server.closeIdleConnections(), so keep-alive sockets do not hold the server open.
  4. Finish in-flight work: wait for running requests and jobs.
  5. Close resources: database pools, open files, message queues.
  6. Stop workers and child processes: worker.terminate(), child.kill("SIGTERM").
  7. Shutdown timeout: if it all takes too long, log it and force process.exit(1).

Click ▶ Run, send GET /report, then press Send SIGTERM.

  • server.close(callback) = the callback runs after the last open request is done.
  • force.unref() = the safety timer does not keep the program alive by itself.
  • The stopping flag = a second signal does not start a second shutdown.

Simple lesson: on SIGTERM, close the server, finish requests, close resources, then exit 0.

Performance Tuning

  • --max-http-header-size=32768 = allow bigger request headers (the default is 16 KB). Raise it only if real clients need it.
  • Compile cache = Node.js saves compiled code to disk, so the next start is faster. Turn it on with module.enableCompileCache(), or NODE_COMPILE_CACHE=dir.
// main.mjs
import module from "node:module";

const result = module.enableCompileCache();
console.log("Compile cache on?", result.status === module.constants.compileCacheStatus.ENABLED || result.status === module.constants.compileCacheStatus.ALREADY_ENABLED);

Output:

Compile cache on? true

Simple lesson: change tuning flags only when you have measured a real need.

Logging and Monitoring

Structured (JSON) Logging

Structured log = one JSON object per line, with fixed fields such as time, level and msg.

  • People can still read it, and machines can search and count it.
  • Log tools such as Grafana Loki, Elasticsearch and cloud consoles read JSON lines directly.
  • Libraries such as pino write fast JSON logs.

Log Levels

  • error = something failed and needs attention.
  • warn = something odd, but the app goes on.
  • info = normal important events: started, stopped, user created.
  • debug = details for developers. Usually off in production.
  • Set the level with an environment variable, such as LOG_LEVEL=info.
  • Never log passwords, tokens or full card numbers.

Logs are kept for months and read by many people. Treat them as public.

  • Add a request id to every line, so you can follow one request (see AsyncLocalStorage in Events and Utilities).

diagnostics_channel (Advanced)

  • node:diagnostics_channel = named channels where libraries publish events, such as "HTTP request started".
  • Monitoring tools subscribe to them, without changing your code.
  • Node.js itself publishes on channels like http.server.request.start.
// main.mjs
import diagnostics_channel from "node:diagnostics_channel";

const channel = diagnostics_channel.channel("school:fee-paid");
diagnostics_channel.subscribe("school:fee-paid", (msg) => console.log("monitor saw:", msg));

channel.publish({ student: "Asha", amount: 2500 });

Output:

monitor saw: { student: 'Asha', amount: 2500 }

Simple lesson: log one JSON object per line, choose the level with an environment variable.

Upgrading Node.js

Safe Upgrade Steps

  1. Read the release notes of the new major version, especially breaking changes.
  2. Upgrade on your own computer with nvm or fnm first.
  3. Run the tests, and run the app with --trace-deprecation to find old APIs.
  4. Update the engines field and the Node.js version in Docker and CI.
  5. Deploy to a test environment, then production.

CI Version Matrix

  • CI matrix = run your tests on several Node.js versions at once, such as the current LTS and the next one.
  • This shows problems before you upgrade.
# .github/workflows/test.yml (part)
strategy:
  matrix:
    node-version: [22, 24]
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node-version }}
  - run: npm ci
  - run: npm test

Deprecations and Security Releases

  • --trace-deprecation = show where a deprecated API is used, with a stack trace.
  • --throw-deprecation = turn deprecation warnings into errors, so tests fail on them.
  • Security releases = Node.js publishes fixes for security problems. Subscribe to the Node.js security announcements and update within days.

old.js

const { deprecate } = require("node:util");
const oldReport = deprecate(() => "report", "oldReport() is deprecated. Use buildReport().", "DEP_SCHOOL_001");
oldReport();
$ node --throw-deprecation old.js
DeprecationWarning: oldReport() is deprecated. Use buildReport().

Simple lesson: upgrade on your computer first, test on several versions, and never ignore security releases.

Distribution (Advanced)

  • Single executable application (SEA) = one file that holds Node.js and your app. Users run it without installing Node.js.
  • Bundling (with esbuild or Rollup) = combine your code and its packages into one file. Smaller and faster to start.
  • Shipping node_modules = simpler, and works for every package.
  • Native modules = packages with C++ code (.node files). They cannot go inside a bundle, and they must be built for each operating system.

Simple lesson: ship node_modules by default, and bundle or build a SEA only when you need to.

Pre-Production Checklist

The full checklist:

  • Security: secrets in environment variables, npm audit clean, least permissions, HTTPS in front.
  • Reliability: config validated at start-up, uncaughtException handled with fail fast, graceful shutdown, health checks.
  • Observability: structured JSON logs with levels, diagnostic reports on fatal errors.
  • Performance: memory limit tested under load, event loop delay measured.
  • Operations: LTS version pinned, engines set, lockfile committed, npm ci in builds.

Simple lesson: run a checklist before every first launch, and automate what you can.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
CMD ["npm", "start"] in Dockernpm passes signals onRun node server.js directlydocker stop kills the app hard
No SIGTERM handlerNode.js stops cleanly by itselfHandle SIGTERM and close the serverRequests cut off during deploys
Shutdown with no timeoutClosing always finishesAdd a forced exit timerThe app hangs and is killed anyway
Checking settings when first usedLazy is fineValidate all settings at start-upErrors show up hours later
console.log text logs in productionText is easy to readJSON lines with levelsLogs cannot be searched or counted
Expecting NODE_ENV=production to speed up Node.jsNode.js reads itOnly libraries read itFalse sense of tuning
Skipping security releasesUpdates can waitUpgrade within daysKnown holes stay open

TL;DR

  1. NODE_ENV = read by libraries, not by Node.js itself.
  2. Validate config at start-up. Fail fast with exit code 1.
  3. Process manager = restarts crashes. One process per container.
  4. PID 1 = handle SIGTERM, or use --init.
  5. Health checks = liveness (restart me) and readiness (send me traffic).
  6. Graceful shutdown = SIGTERM → server.close() → finish work → close resources → exit 0.

    • plus a forced-exit timeout
  7. Logs = JSON lines with level. Choose the level with LOG_LEVEL.
  8. Upgrades = test locally, CI matrix, --trace-deprecation, security releases.
  9. Checklist = security, reliability, observability, performance, operations.

Check Yourself

Exercises

What to Remember

  • Validate settings at start-up and fail fast.
  • Let a process manager or the platform restart crashes, and run one process per container.
  • Expose liveness and readiness health checks.
  • On SIGTERM: close the server, finish requests, close resources, exit 0, with a timeout.
  • Log JSON lines with levels, and never log secrets.
  • Upgrade on your computer first, test across versions, and apply security releases quickly.

Next: Capstone: Build a Report Card Service. You will put the whole course together in one project.