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.

202 lines8.1 KB Glm Backend
targetModels
GLM-5.3GLM-5.2GLM-5 FamilyGLM-4.6Future GLM 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 GLM: scripts/model-profiles.json -->

#Task boundary

  1. Implement only what the task names; no extra abstractions or files.
  2. English-only comments and identifiers.
  3. Stop when the checklist passes.

#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()]);
  1. Promise.all rejects on the first failure; Promise.allSettled when you need every result. Use Promise.all unless partial success is meaningful.
  2. 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.
  3. Bound concurrent fan-out (p-limit, a semaphore). Promise.all over 10,000 items opens 10,000 sockets.
  4. An unawaited promise that rejects becomes an unhandledRejection, which terminates the process by default in current Node. Await it or attach a handler.
  5. 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.

  1. Commit the lockfile and install with npm ci, never npm install, in CI and in Docker builds.
  2. Pin the Node version in .nvmrc and engines, and run the same major in CI as in production.
  3. Audit in CI (npm audit --audit-level=high, Dependabot, Snyk) and treat a critical finding as a build failure.
  4. 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.
  5. Use --ignore-scripts where feasible; postinstall scripts are the most common malicious-package vector.
  6. 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

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