Claude Fable 5.1 & GPT-6 Astra packages are live

Node

Free

Node.js server rules — the event loop, async correctness, streams and backpressure, process configuration, and the supply chain.

199 lines8.2 KB Minimax Backend
targetModels
MiniMax M3MiniMax M2MiniMax M FamilyFuture MiniMax Models
name
node
category
Backend
description
Node.js server rules — the event loop, async correctness, streams and backpressure, process configuration, and the supply chain.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for MiniMax: scripts/model-profiles.json -->

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names; report any out-of-scope change instead of making it.


#Purpose

Rules specific to running Node.js as a backend. Node's model — one thread running an event loop — is the source of both its throughput and every performance surprise it produces.

Framework-level concerns are Backend/express; this is the runtime.


#Never block the event loop

One thread serves every request. A synchronous operation stops all of them.

js
// Blocks every concurrent request for the duration
const data = fs.readFileSync("./large.json");
const hash = crypto.pbkdf2Sync(pw, salt, 600_000, 32, "sha512");

// Non-blocking
const data = await fs.promises.readFile("./large.json");
const hash = await promisify(crypto.pbkdf2)(pw, salt, 600_000, 32, "sha512");

Common blockers, in order of how often they appear in production:

OperationCost
JSON.parse on a multi-MB payloadTens to hundreds of ms, synchronous
Synchronous crypto (pbkdf2Sync, scryptSync)Hundreds of ms by design
readFileSync in a request pathDisk latency, blocking
A regex with catastrophic backtrackingUnbounded — a ReDoS vector
A large Array.sort or for loopMilliseconds per 100k elements
zlib sync variantsProportional to payload size

Startup is the exception: readFileSync at module load is fine, because nothing is being served yet.

For genuinely CPU-bound work use worker_threads or a separate service. Adding concurrency to a blocked event loop does nothing. Monitor event-loop delay (perf_hooks.monitorEventLoopDelay) — a p99 above ~50 ms means something is blocking. → Backend/monitoring


#Async correctness

js
// Sequential — 3× slower than necessary when the calls are independent
const a = await getA(); const b = await getB(); const c = await getC();

// Concurrent, and it does not lose an error when one rejects
const [a, b, c] = await Promise.all([getA(), getB(), getC()]);
  • Promise.all rejects on the first failure; Promise.allSettled when you need every result. Use Promise.all unless partial success is meaningful.
  • Never use forEach with an async callback — it does not await, so the function returns before the work finishes and errors are unhandled. Use for…of for sequential, Promise.all(map(...)) for concurrent.
  • Bound concurrent fan-out (p-limit, a semaphore). Promise.all over 10,000 items opens 10,000 sockets.
  • An unawaited promise that rejects becomes an unhandledRejection, which terminates the process by default in current Node. Await it or attach a handler.
  • Wrap EventEmitter callbacks: an exception thrown inside one is not caught by the surrounding try.

#Streams and backpressure

Reading a large file or response body into memory works in testing and OOMs in production.

js
// Buffers the whole file into memory
res.send(await fs.promises.readFile(path));

// Streams it, and `pipeline` propagates errors and destroys sockets correctly
await pipeline(fs.createReadStream(path), res);

pipeline (not .pipe()) handles error propagation and cleanup — a .pipe() chain leaks file descriptors when the destination errors.

Backpressure is the reason streams exist: if you ignore the return value of write(), a fast producer will fill memory until the process dies. pipeline handles it for you.


#Process configuration

dockerfile
ENV NODE_ENV=production
# Set the heap below the container limit, or the OOM killer arrives before GC does
ENV NODE_OPTIONS="--max-old-space-size=768"
SettingWhy
NODE_ENV=productionFrameworks skip development-only work; error pages stop leaking traces
--max-old-space-sizeDefault heap ignores cgroup limits; set it ~75% of the container limit
UV_THREADPOOL_SIZEDefault 4 threads serve fs, dns and crypto — raise if those queue
Process managerOne process per core (Kubernetes replicas, not cluster, in containers)
--enable-source-mapsReadable stack traces from compiled TypeScript

Handle SIGTERM: stop accepting connections, drain in-flight requests with a bounded timeout, close the database pool, exit. Without it every deploy severs live requests. → DevOps/deployment

unhandledRejection and uncaughtException should log and exit — after an uncaught exception the process state is unknown. → Backend/error-handling


#Dependencies

Node's supply chain is the largest of any ecosystem, and it is a real attack path.

  • Commit the lockfile and install with npm ci, never npm install, in CI and in Docker builds.
  • Pin the Node version in .nvmrc and engines, and run the same major in CI as in production.
  • Audit in CI (npm audit --audit-level=high, Dependabot, Snyk) and treat a critical finding as a build failure.
  • Prefer the standard library. node:crypto, node:test, fetch, AbortSignal and structuredClone are built in — a dependency for what a few lines can do is a permanent liability.
  • Use --ignore-scripts where feasible; postinstall scripts are the most common malicious-package vector.
  • Never run the process as root in a container; use a non-root user and a read-only filesystem. → DevOps/docker

#Anti-patterns

Anti-patternWhy it failsFix
Sync I/O or crypto in a request pathBlocks every concurrent requestAsync variants
Adding concurrency to CPU-bound workThe event loop is still one threadworker_threads or a separate service
forEach with an async callbackDoes not await; errors unhandledfor…of or Promise.all
Sequential independent awaitsLatency adds up needlesslyPromise.all
Unbounded Promise.all fan-outThousands of sockets at onceConcurrency limiter
Floating promisesProcess-terminating unhandled rejectionAwait or handle
.pipe() without error handlingLeaked descriptors on failurepipeline
Reading large files into memoryOOM at production sizesStream
No --max-old-space-sizeOOM killer before GC runsSet below the container limit
Missing NODE_ENV=productionDevelopment error pages leak tracesSet it explicitly
No SIGTERM handlerEvery deploy severs live requestsGraceful shutdown
Continuing after uncaughtExceptionUnknown process stateLog and exit
npm install in CIIgnores the lockfile; irreproducible buildsnpm ci
Running as root in a containerContainer escape has full privilegesNon-root user
A dependency for a one-linerSupply-chain surface for nothingStandard library

#Checklist

  • Verify: No synchronous I/O, crypto or compression in request paths
  • Verify: CPU-bound work runs in worker threads or a separate service
  • Verify: Event-loop delay is monitored and alerted on
  • Verify: Independent async work uses Promise.all, with bounded fan-out
  • Verify: No async callbacks passed to forEach
  • Verify: No floating promises; rejections are handled
  • Verify: Large payloads are streamed with pipeline, not buffered
  • Verify: NODE_ENV=production is set in production images
  • Verify: Heap size is set below the container memory limit
  • Verify: UV_THREADPOOL_SIZE is tuned if fs/dns/crypto work queues
  • Verify: SIGTERM triggers a graceful drain and clean exit
  • Verify: unhandledRejection and uncaughtException log and exit
  • Verify: The lockfile is committed and CI installs with npm ci
  • Verify: The Node version is pinned and matches between CI and production
  • Verify: Dependency audits run in CI and fail on high-severity findings
  • Verify: The container runs as a non-root user