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:
- What
NODE_ENVchanges, and how to validate settings - Process managers, containers and PID 1
- Health check endpoints
- Graceful shutdown, step by step
- Shutdown timeouts and forced exit
- Tuning flags and the compile cache
- Structured JSON logs, log levels and
diagnostics_channel - Upgrading Node.js safely
- 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_LEVELorCACHE_SECONDS, rather than checkingNODE_ENVeverywhere.
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_URLline from.envand 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.jsdirectly, notnpm 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
SIGTERMhandler,docker stopwaits and then kills it hard. - Fix it: handle
SIGTERMyourself, or start withdocker run --init(ortini), 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
200when healthy,503when not.
- Right after start,
/health/readysaysstartingwith status 503, because the pretend database is not connected yet. - Send it again a moment later. Now it says
readywith 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
- Wire shutdown to signals:
process.on("SIGTERM", shutdown)andSIGINT. - Stop new connections:
server.close(). Requests already started keep going. - Close idle connections:
server.closeIdleConnections(), so keep-alive sockets do not hold the server open. - Finish in-flight work: wait for running requests and jobs.
- Close resources: database pools, open files, message queues.
- Stop workers and child processes:
worker.terminate(),child.kill("SIGTERM"). - 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
stoppingflag = 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(), orNODE_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
AsyncLocalStoragein 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
- Read the release notes of the new major version, especially breaking changes.
- Upgrade on your own computer with nvm or fnm first.
- Run the tests, and run the app with
--trace-deprecationto find old APIs. - Update the
enginesfield and the Node.js version in Docker and CI. - 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 (
.nodefiles). 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 auditclean, least permissions, HTTPS in front. - Reliability: config validated at start-up,
uncaughtExceptionhandled 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,
enginesset, lockfile committed,npm ciin builds.
Simple lesson: run a checklist before every first launch, and automate what you can.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
CMD ["npm", "start"] in Docker | npm passes signals on | Run node server.js directly | docker stop kills the app hard |
| No SIGTERM handler | Node.js stops cleanly by itself | Handle SIGTERM and close the server | Requests cut off during deploys |
| Shutdown with no timeout | Closing always finishes | Add a forced exit timer | The app hangs and is killed anyway |
| Checking settings when first used | Lazy is fine | Validate all settings at start-up | Errors show up hours later |
console.log text logs in production | Text is easy to read | JSON lines with levels | Logs cannot be searched or counted |
Expecting NODE_ENV=production to speed up Node.js | Node.js reads it | Only libraries read it | False sense of tuning |
| Skipping security releases | Updates can wait | Upgrade within days | Known holes stay open |
TL;DR
NODE_ENV= read by libraries, not by Node.js itself.- Validate config at start-up. Fail fast with exit code 1.
- Process manager = restarts crashes. One process per container.
- PID 1 = handle SIGTERM, or use
--init. - Health checks = liveness (restart me) and readiness (send me traffic).
Graceful shutdown = SIGTERM →
server.close()→ finish work → close resources → exit 0.- plus a forced-exit timeout
- Logs = JSON lines with
level. Choose the level withLOG_LEVEL. - Upgrades = test locally, CI matrix,
--trace-deprecation, security releases. - 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.