Node.js and Express: Technical Architecture Review
Review date:May 26, 2026
Scope:Node.js 24.19.0 LTS, Node.js 26.7.0 Current, Express 5.2.1, the maintained Express 4.x line, and current official Node.js and Express documentation.
Executive assessment
The most useful claim in the original review is also the one worth preserving:Node.js owns the network runtime; Express organizes application-level request handling. An Express application is a callable request listener.app.listen()is a convenience that creates a Nodehttp.Server, passes the Express application to it, starts listening, and returns the server object. Express therefore is not an independent transport stack, but neither is it merely a collection of unrelated helpers. It supplies a coherent middleware, routing, request, response, and error-handling model on top of Node's HTTP primitives.[^1][^2]
That distinction explains why Node.js and Express fit I/O-oriented APIs so well. Node can keep many network operations in flight without allocating one JavaScript thread per request. Express adds little ceremony to the path from an incoming request to middleware, routing, domain logic, and a response. The result is especially effective when request handlers perform modest synchronous work and spend much of their lifetime awaiting databases, caches, queues, object storage, or other services.[^3]
The same architecture has clear limits. Anasyncfunction does not make CPU work parallel. A large synchronous loop, expensive regular expression, huge JSON parse or serialization, or CPU-heavy transformation still occupies the event-loop thread and delays unrelated requests. Selected native operations use libuv's worker pool, but that pool is finite and is not a general execution engine for arbitrary JavaScript. Worker threads, processes, or external workers are needed when CPU work is substantial.[^3][^4]
Express is intentionally minimal. It does not define a database layer, schema system, authentication strategy, authorization model, transaction boundary, queue architecture, or application folder structure. This is architectural freedom, not missing implementation. It also means that production quality depends heavily on conventions supplied by the application team. Middleware order, proxy trust, input validation, timeout policy, cancellation, concurrency limits, state ownership, and shutdown behavior are all part of the system design rather than properties Express can infer automatically.[^5][^6]
The original article was technically ambitious but much too long. It repeated the same conclusions across 141 numbered sections, mixed framework facts with recommended application patterns, and ended with precise numerical ratings without a scoring method. This revision consolidates the review, distinguishes official behavior from engineering advice, and treats version-specific statements as dated claims.
1. Product boundary: what Node.js does and what Express does
A minimal Express program can look like this:
importexpressfrom"express";
constapp = express();
app.get("/health", (_req, res) => {
res.json({ status:"ok"});
});
constserver = app.listen(3000);The short source listing hides several layers:
Client
-> operating-system socket and TCP/TLS
-> Node.js HTTP parser and http.Server
-> Express application function
-> ordered middleware and router stack
-> route handler or error middleware
-> Node.js ServerResponse
-> clientexpress()returns a function that can be supplied directly tohttp.createServer(). In Express 5.2.1,app.listen()callshttp.createServer(this), attaches any callback, and invokes the server'slisten()method.[^1][^2]The precise formulation is therefore:
>Node.js provides the process, event loop, sockets, HTTP implementation, streams, buffers, workers, and operating-system integration. Express provides an application-level request pipeline over Node's HTTP request and response objects.
This separation is useful operationally. The application can configure Node's server-level timeouts, connection handling, TLS termination, and shutdown through the returnedhttp.Server, while Express controls routing and middleware.
Express's minimalism is deliberate. The framework includes routing, nested routers, static-file middleware, body parsers, request and response conveniences, and an error middleware convention. It does not mandate controllers, services, repositories, dependency injection, an ORM, or an API schema language.[^5]A controller-service-repository structure can be a sensible application convention, but it is not an Express execution requirement and should not be described as though the framework enforces it.
A practical application boundary
For a medium or large API, a defensible separation is:
HTTP and Express layer
- routing
- protocol-specific parsing
- authentication context
- request validation
- status codes and response representation
Domain/application layer
- use cases
- authorization decisions
- transactions
- idempotency
- orchestration of dependencies
Infrastructure layer
- database and cache clients
- queues
- object storage
- external services
- telemetry exportersThis arrangement keeps Express replaceable and stopsreqandresfrom spreading into business logic. It is an engineering recommendation, not a universal law. Small services may reasonably use fewer layers.
2. The concurrency model: event loop, worker pool, threads, and processes
One primary JavaScript event loop per isolate
A normal Node server process has one primary JavaScript event loop for its main isolate. It does not create one JavaScript thread per HTTP request. Instead, callbacks run to completion on the event-loop thread, and asynchronous network operations are coordinated through the operating system and libuv.[^3]
This makes high concurrency possible when callbacks remain short:
Request A -> starts database operation -> yields
Request B -> starts cache operation -> yields
Request C -> returns a small response
Database A completes -> continuation for A runs
Cache B completes -> continuation for B runsThe wordconcurrencyis important. Several operations may be in progress, but two ordinary JavaScript callbacks do not execute simultaneously on the same event-loop thread. This is different from parallel JavaScript execution.
async does not move CPU work
Anasyncfunction returns a Promise. It does not automatically create a thread:
asyncfunctioncalculate() {
lettotal =0;
for(leti =0; i <5_000_000_000; i +=1) {
total += i;
}
returntotal;
}The loop still blocks the event loop.awaitcreates an opportunity for other work only when the awaited operation actually completes asynchronously or when the Promise chain yields. Large microtask orprocess.nextTick()chains can also delay other event-loop phases, so avoiding only synchronous APIs is not sufficient; callback and continuation work must remain bounded.
This is both a performance and security issue. Node's own guidance connects expensive callbacks and worker-pool tasks with denial-of-service risk because attacker-controlled input may trigger disproportionate computation.[^3]
The libuv worker pool is selective
The worker pool serves selected native operations, including many filesystem calls, some cryptographic operations, compression, anddns.lookup(). Ordinary socket I/O is generally evented rather than assigned to one pool thread per request. The generic statement that “DNS runs in the worker pool” is therefore too broad:dns.lookup()typically uses the system resolver through the pool, whereasdns.resolve*()uses c-ares and follows a different path.
Worker-pool saturation differs from event-loop blocking. The event loop may remain responsive while expensive pool tasks queue behind one another. Password hashing, compression, or large filesystem workloads can therefore produce high latency without a visibly blocked JavaScript loop. IncreasingUV_THREADPOOL_SIZEcan help a measured workload, but it is not a universal optimization; it also increases concurrency against CPU, memory, storage, or external systems.
Worker threads are for CPU-intensive JavaScript
The stablenode:worker_threadsmodule runs JavaScript in parallel and can transfer or share memory. Node's documentation explicitly says workers are useful for CPU-intensive JavaScript and generally do not improve I/O-intensive work, for which built-in asynchronous APIs are already more efficient.[^4]
Creating one worker per request is usually inefficient. Worker startup and data transfer can cost more than the work itself. A bounded worker pool is the normal design for repeated CPU jobs:
HTTP request
-> validate small request
-> submit bounded job to worker pool
-> await result or deadline
-> return responseJobs should have queue limits, timeouts, cancellation behavior, memory limits, and overload responses. Otherwise, moving computation off the event loop merely moves the unbounded queue elsewhere.
Processes and cluster
Node'sclustermodule remains stable. It creates child processes that can share server ports. Each worker has its own V8 heap, event loop, garbage collector, and failure boundary. The current documentation recommends worker threads when process isolation is unnecessary.[^7]
clusteris only one deployment option. A common contemporary topology is one Node process per container or service unit, replicated by Kubernetes, systemd, a cloud platform, or another orchestrator behind a load balancer. The essential principle is process replication, not use of one particular Node API.
Process-local state does not become shared merely because several workers serve one endpoint. Sessions, rate limits, caches, locks, and job ownership need an explicit distributed design when instances multiply.
3. Middleware and routing are the application control flow
Express describes an application as a sequence of middleware calls. Middleware can inspect or modify the request and response, end the response, or pass control withnext().[^5]Registration order is therefore executable control flow.
This is powerful because cross-cutting policy maps naturally onto a pipeline:
request context and logging
-> coarse connection or abuse controls
-> route match
-> route-appropriate parser
-> authentication
-> authorization and schema validation
-> controller
-> domain service
-> response
-> error handlingThat order is illustrative, not universal. For example, an endpoint may need a signed raw request body before JSON parsing; another may validate a small routing parameter before performing expensive authentication; a public route may not authenticate at all. The correct rule is to make order deliberate, documented, and tested.
Routers are useful domain boundaries
express.Router()behaves like a miniature application with middleware and routes. Routers are most useful when they represent coherent domains such as users, billing, experiments, or devices rather than serving only as a way to split a large file.
Nested routers should make inherited policy visible. A reader should be able to answer:
-Which authentication middleware protects this route?
-Which parser and body limit apply?
-Which rate-limit policy applies?
-What validation is performed?
-Which error handler converts failures to the public API format?
Large Express codebases become difficult when policy is distributed through implicitreqmutation, distantapp.use()calls, and routers whose mount order is not obvious. The framework permits such designs; it does not require them.
Route-path changes in Express 5
Express 5 changed string route-path syntax. Wildcards must be named, optional segments use braces rather than the old?form, and regular-expression characters are not supported inside string paths in the same way as Express 4.[^8]Migrations with complex wildcard or regex-like paths therefore require tests. A successful package upgrade does not prove that the old routing table still matches the same URLs.
4. Parsing, validation, authentication, and authorization are separate concerns
express.json(),express.urlencoded(),express.text(), andexpress.raw()turn bytes into JavaScript values. They do not establish that the resulting value is valid or authorized for the application.
The sequence should be understood as:
bytes
-> content-type and content-encoding handling
-> size-limited parser
-> untrusted JavaScript value
-> schema and semantic validation
-> authenticated and authorized useA request body can be valid JSON and still be malicious or nonsensical. Query strings, route parameters, headers, cookies, filenames, and uploaded files are equally untrusted. TypeScript types do not validate network data at runtime.
Limit parsers by route
One global maximum body size is often either too permissive or too restrictive. A login endpoint may need a few kilobytes, while an import endpoint may legitimately need more. Route-scoped parsers make resource policy explicit.
Compressed input also needs attention. Express 5 body parsers support gzip, deflate, and Brotli decompression.[^8]A small compressed body can expand dramatically. Limits, proxy controls, and decompression behavior should therefore be tested together rather than evaluated only against the transmitted byte count.
For genuinely large objects, a streaming upload path or direct-to-object-storage design is often better thanexpress.json()or a large in-memory multipart parser.
Authentication and authorization are different
Authentication establishes who or what is calling. Authorization decides whether that identity may perform a particular action on a particular resource. Express provides middleware locations for both, but no built-in policy model.
Authorization should usually occur in or near the domain use case, where resource ownership and business state are available. A route-level role check alone may not be enough for object-level permissions.
CORS is not access control
CORS controls browser cross-origin behavior. It does not authenticate a caller and does not stop scripts, servers, command-line clients, or compromised browsers from sending requests directly. It should be described as browser policy, not as an API security boundary.
5. Error handling: what Express 5 captures and what it does not
Express 5 substantially improves Promise-based error propagation. If a route handler or middleware returns a Promise and that Promise rejects, Express automatically forwards the rejection to the error pipeline.[^9]
app.get("/users/:id",async(req, res) => {
constuser =awaitloadUser(req.params.id);
res.json(user);
});IfloadUser()rejects, the error reaches error-handling middleware without a manualtry/catcharound the handler.
The important condition isreturned Promise chain. Express cannot automatically capture arbitrary asynchronous work detached from that chain:
app.get("/report", (_req, res, next) => {
setTimeout(() => {
try{
thrownewError("late failure");
}catch(error) {
next(error);
}
},10);
res.status(202).end();
});Timers, callbacks, EventEmitter'error'events, stream failures, background tasks, and fire-and-forget Promises need explicit ownership. The application must decide whether a failure belongs to the current request, a background job, or the process supervisor.
Errors after headers are sent
Once a response has started, Express may no longer be able to send a clean JSON error document. The official guide recommends delegating to the default handler whenres.headersSentis true; the default handler may close the connection for an error encountered while streaming.[^9]
A production error layer should:
-expose a stable public error schema;
-avoid leaking stacks, secrets, SQL, filesystem paths, or internal service responses;
-preserve a private cause and correlation identifier in logs;
-distinguish client errors, conflicts, dependency failures, timeouts, and internal failures;
-handle aborted requests and streaming failures;
-avoid trying to keep an uncertain process alive after a truly fatal uncaught failure.
An error middleware is a protocol adapter, not a substitute for a process restart strategy.
6. Streams, backpressure, JSON, and memory
Node's HTTP interfaces are stream-oriented and deliberately do not require entire requests or responses to be buffered.[^10]This makes Node well suited to file transfers, exports, proxies, and incremental transformations.
Streaming is not automatic merely because the runtime supports streams.express.json()buffers and parses a body;res.json()serializes a complete JavaScript value. A route that loads a four-gigabyte file into memory and then callsres.send()is still a buffered design.
Honor backpressure
A writable stream'swrite()method returnsfalsewhen the producer should wait for'drain'. Ignoring this signal can cause unbounded buffering and memory growth.[^11]stream.pipeline()is generally preferable for connecting streams because it coordinates backpressure and error propagation.
import{ pipeline }from"node:stream/promises";
import{ createReadStream }from"node:fs";
app.get("/exports/:name",async(req, res, next) => {
try{
res.type("application/octet-stream");
awaitpipeline(createReadStream(resolveExport(req.params.name)), res);
}catch(error) {
next(error);
}
});Path authorization and traversal protection remain required; the example shows flow control, not a complete secure endpoint.
Large JSON is a different workload
JSON parsing and stringification are synchronous CPU and memory operations in common application paths. A large object can block the event loop, create several in-memory copies, and trigger garbage collection. For large result sets, consider pagination, newline-delimited JSON, server-sent events, streamed database cursors, files, or asynchronous export jobs.
A streaming design also needs disconnect handling. When the client aborts, continuing database scans, object-store reads, or transformations may waste resources. Where dependencies support it, anAbortSignalor equivalent cancellation mechanism should propagate from request lifetime to downstream work.
7. HTTP hardening, timeouts, proxies, and the network edge
Express does not replace Node's HTTP-server controls. Node 26 defaultsserver.requestTimeoutto 300,000 milliseconds andserver.headersTimeoutto the smaller of 60,000 milliseconds orrequestTimeout.[^12]These protect the time allowed to receive request headers and the complete request from slow or incomplete clients.
They donotimpose a deadline on application logic after the request has been delivered to Express. They also do not automatically limit a database query,fetch(), Redis operation, queue call, or internal service request. Node's general socket inactivity timeout (server.timeout) remains a separate setting and defaults to no inactivity timeout. A production system therefore needs several layers:
-header and request-receipt limits;
-socket and keep-alive policy;
-route-specific application deadlines;
-downstream client timeouts;
-cancellation;
-bounded retries and idempotency;
-reverse-proxy limits;
-overload and admission control.
Timeouts are not retries. Retrying every timeout can amplify an outage. Retry budgets should be limited to operations that are safe to repeat, and jittered backoff should be coordinated with circuit-breaking or fail-fast behavior where appropriate.
Reverse proxies and trust proxy
A reverse proxy or API gateway may own TLS, HTTP/2 or HTTP/3 at the edge, connection buffering, WAF rules, request limits, compression, caching, and load balancing. The application should document which layer owns each policy to avoid contradictory limits or headers.
Express'strust proxysetting changes how client IP, protocol, and host information are derived. The official documentation warns that the setting must match the exact proxy topology because forwarded values become trusted application input.[^13]
trust proxy: trueis safe only when the last trusted proxy removes or overwrites incomingX-Forwarded-*headers. Numeric hop counts can be unsafe if clients can reach the application through paths of different lengths. Named subnet or custom trust functions are often clearer for complex deployments.
Incorrect proxy trust can undermine:
-secure-cookie decisions;
-HTTPS redirects;
-IP-based rate limits;
-audit logs;
-geolocation;
-access policy.
Proxy configuration is therefore part of the security model, not a deployment footnote.
8. Scaling requires bounded concurrency and externalized state
Node can efficiently maintain many waiting operations. It cannot make databases, caches, payment services, queues, or CPUs infinitely fast.
A route such as this can create a dependency flood:
awaitPromise.all(ids.map((id) => loadFromDatabase(id)));For two or three fixed independent calls, concurrency can reduce latency. When the number of operations is controlled by user input, it should be bounded. Otherwise thousands of requests may each create thousands of downstream operations.
A complete concurrency policy includes:
-request admission limits;
-per-route concurrency;
-database-pool size;
-outbound HTTP-agent limits;
-queue depth;
-worker-pool size;
-rate limits by identity and operation;
-backpressure responses;
-deadlines and cancellation.
Horizontal replicas require shared or partitioned state
With several processes or containers, process-local memory is not a reliable global session store, rate limiter, job lock, or cache. State must be externalized, partitioned, or deliberately made instance-affine.
An in-memory rate limit of 100 requests per process becomes 300 effective requests across three independent instances unless the instances coordinate. IP-based limits also depend on correct proxy trust and may be inappropriate behind shared networks or carrier NAT. User-, token-, tenant-, and operation-based policies are often more meaningful.
Transactions and side effects
Express does not define database transactions. A service or use-case layer should own transaction boundaries. External effects such as email, payment calls, and published messages cannot usually be undone by a database rollback. Reliable designs may need idempotency keys, an outbox, sagas, compensation, deduplication, and exactly-onceeffectsbuilt on at-least-once delivery rather than vague promises of exactly-once transport.
These are distributed-system concerns above Express. Framework simplicity should not be confused with system simplicity.
9. Security is an application and deployment property
Express's production security guide recommends current dependency versions, TLS, input validation, Helmet or equivalent hardened headers, secure cookies, protection of authentication endpoints, and dependency auditing.[^14]These are useful controls, but they are not a complete security model.
A defensible API design should address:
-endpoint authentication and object-level authorization;
-runtime validation of every untrusted input source;
-parser and upload limits;
-safe file and path handling;
-SQL, command, template, and header injection;
-SSRF and outbound-request policy;
-secrets management;
-CSRF where browser cookies carry credentials;
-CORS as browser policy;
-secure cookies and session fixation;
-abuse controls and distributed rate limiting;
-dependency provenance and lockfiles;
-log redaction;
-error-message redaction;
-proxy trust;
-timeouts and algorithmic-complexity attacks.
Middleware packages should be evaluated as dependencies, not treated as magical security labels. Their configuration, maintenance, transitive dependencies, and ordering matter.
10. Observability, request context, health, and shutdown
AsyncLocalStorage is a request-context mechanism
AsyncLocalStorageis stable and is designed to associate state with asynchronous callbacks and Promise chains, similar to thread-local storage in other ecosystems.[^15]It is useful for correlation IDs, tenant context, trace identifiers, and structured logging.
run()is normally preferable to casual use ofenterWith()because it scopes context to a callback and its asynchronous descendants. Some custom thenables, callback APIs, native addons, or unusual integration layers can lose context;AsyncResourceor explicit propagation may be needed. Context also does not cross a process, queue, or network boundary automatically. Correlation identifiers and trace context must be serialized into outbound requests and messages.
Measure the event loop and the dependencies
Useful production signals include:
-request rate and concurrent requests;
-median, p95, and p99 latency;
-error and timeout rates;
-event-loop delay and utilization;
-worker-pool or worker-thread queue depth;
-heap, RSS, garbage collection, and memory growth;
-database-pool saturation;
-outbound connection-pool saturation;
-queue lag;
-response sizes and serialization time;
-dependency latency by operation.
A throughput number without tail latency and resource saturation is incomplete.
Liveness and readiness are distinct
Liveness answers whether the process should be restarted. Readiness answers whether the process should receive new traffic. Express's production guide describes these as distinct deployment checks.[^16]
Liveness should be cheap and should not create a dependency load test. Readiness may include selected critical conditions such as initialization completion or an unusable database state, but its failure policy should be chosen carefully to avoid removing every replica during a shared dependency outage.
Graceful shutdown needs a deadline
OnSIGTERM, a production service should normally:
1.become unready and leave the load balancer;
2.stop accepting new connections;
3.allow in-flight requests to finish within a deadline;
4.stop consumers and schedulers;
5.close database, cache, queue, and telemetry clients;
6.handle idle keep-alive and upgraded connections deliberately;
7.force termination after a bounded grace period.
Express's example covers stopping the server and completing ongoing requests, but a real application must also close its own resources.[^16]WebSockets, server-sent events, long polling, and HTTP upgrades need explicit shutdown handling.
Processes should be disposable. A supervisor should replace a process after an uncaught fatal error rather than assuming its in-memory state remains trustworthy.
11. Express 5, supported versions, TypeScript, and native addons
Current baseline on August 11, 2026
The current official version picture is:
| Component | Status |
|---|---|
| Node.js 24.19.0 | Latest LTS line and the conservative production baseline |
| Node.js 26.7.0 | Current release line, not yet the LTS recommendation |
| Express 5.2.1 | Latest Express 5 release |
| Express 4.22.2 | A separately maintained Express 4 release line |
Node's release page explicitly recommends Active or Maintenance LTS releases for production. It also documents a change beginning with Node 27: annual major releases, with each major eventually entering LTS after its Alpha and Current phases.[^17][^18][^19]
Express 5 requires Node 18 or newer, but Node 18 is end-of-life. “Supported by Express” is therefore not the same as “appropriate production runtime today.” For a new conservative deployment, Node 24 LTS is the clearer default. Node 26 may be reasonable when a team intentionally accepts the Current release cadence and tests accordingly.
Express 5.2.1 is the latest Express 5 release and reverted an erroneous query-parser breaking change from 5.2.0. Express 4.22.2 is also being released, so “Express 5 is the modern baseline” should not be read as “Express 4 receives no maintenance.” New work should normally evaluate Express 5, while existing Express 4 systems can follow a planned, tested migration rather than an emergency rewrite.[^19]
Built-in TypeScript support is intentionally limited
Modern Node can run TypeScript through stable type stripping, but this is not a full TypeScript toolchain. Node's documentation distinguishes lightweight built-in support from full support through a third-party package or compiler. Built-in stripping does not type-check code, does not honor a completetsconfig.jsonworkflow, and cannot handle every syntax form that requires transformation.[^20]
TypeScript still does not validate HTTP input at runtime. API schemas and runtime validators remain necessary.
Node-API stability is conditional
Node-API provides ABI stability across Node versions for native addons written against Node-API.[^21]This guarantee should not be generalized to every native addon. Addons tied directly to V8, NAN, or internal libuv behavior may require rebuilding or modification across Node majors.
A reproducible build should record:
-Node version and distribution;
-Express version;
-package-manager version;
-lockfile and integrity data;
-operating system and architecture;
-native addon versions and build provenance;
-build flags and environment variables;
-container or deployment image digest;
-proxy and timeout configuration;
-schema and migration versions.
12. How the stack should be evaluated
A useful benchmark should resemble the intended application rather than a trivialGET /route.
Performance protocol
Measure:
-cold and warm startup;
-sustained throughput;
-median, p95, p99, and maximum latency;
-event-loop delay;
-CPU and per-process memory;
-garbage-collection pauses;
-parser and serialization time;
-connection-pool utilization;
-downstream latency and saturation;
-timeout and cancellation behavior;
-overload recovery;
-graceful deployment with active traffic.
Test several workload classes:
1.small JSON plus fast database access;
2.slow downstream service calls;
3.large JSON parsing and serialization;
4.streaming upload and download;
5.CPU-heavy input requiring workers;
6.authentication and validation overhead;
7.dependency failure and retry behavior;
8.malicious or pathological inputs.
Security protocol
Verify:
-exact Node and Express support status;
-TLS and proxy ownership;
-trust proxycorrectness;
-parser limits and decompression behavior;
-schema validation;
-object-level authorization;
-cookie and CSRF policy;
-CORS policy;
-rate limiting across replicas;
-SSRF and outbound network controls;
-log and error redaction;
-dependency audit and lockfile integrity;
-HTTP, application, and dependency timeouts;
-graceful shutdown and process replacement.
Maintainability protocol
A reviewer should be able to trace one route from registration through middleware, validation, authorization, domain logic, persistence, and error conversion. Hidden request mutation and order-sensitive global middleware should be minimized or documented. The public API schema, runtime validator, implementation, and tests should be generated from or checked against a common contract where practical.
Overall conclusion
Node.js and Express remain a strong combination for APIs whose dominant work is asynchronous I/O orchestration. Node supplies a mature event-driven runtime, HTTP implementation, streams, workers, processes, diagnostics, and native integration. Express adds a compact middleware and routing model that is easy to learn and can scale to substantial systems when application boundaries are explicit.
The stack is not an unlimited parallel-computing environment. It is not a complete application architecture. Express does not supply validation, authentication, authorization, transactions, distributed state, or operational policy. Node does not make large synchronous computation harmless. Those responsibilities must be designed deliberately.
The most defensible final assessment is therefore:
>Node.js and Express are especially effective as a thin, observable HTTP control plane for I/O-oriented services. They are less compelling when most request time is spent on unbounded CPU work, enormous in-memory transformations, or architecture that would benefit from stronger framework-enforced conventions.
A good implementation keeps event-loop work bounded, uses streams with backpressure, places CPU work in an appropriate execution layer, limits concurrency against every dependency, validates and authorizes all untrusted input, configures proxies and timeouts explicitly, externalizes shared state, and treats graceful shutdown and supported runtime upgrades as part of the application architecture.
Primary sources
[^1]: Express 5.2.1 application source, includingapp.listen(): https://raw.githubusercontent.com/expressjs/express/v5.2.1/lib/application.js
[^2]: Express 5.2.1 application factory: https://raw.githubusercontent.com/expressjs/express/v5.2.1/lib/express.js
[^3]: Node.js,Don't Block the Event Loop (or the Worker Pool): https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop
[^4]: Node.js 26.7.0,worker_threads: https://nodejs.org/api/worker_threads.html
[^5]: Express,Using middleware: https://expressjs.com/en/guide/using-middleware.html
[^6]: Express 5.2.1 package metadata and supported Node range: https://raw.githubusercontent.com/expressjs/express/v5.2.1/package.json
[^7]: Node.js 26.7.0,cluster: https://nodejs.org/api/cluster.html
[^8]: Express,Moving to Express 5: https://expressjs.com/en/guide/migrating-5.html
[^9]: Express 5,Error handling: https://expressjs.com/en/5x/guide/error-handling.html
[^10]: Node.js 26.7.0,http: https://nodejs.org/api/http.html
[^11]: Node.js 26.7.0,stream: https://nodejs.org/api/stream.html
[^12]: Node.js 26.7.0 HTTP server timeout properties: https://nodejs.org/api/http.html#serverrequesttimeout
[^13]: Express,Express behind proxies: https://expressjs.com/en/guide/behind-proxies.html
[^14]: Express,Production best practices: security: https://expressjs.com/en/advanced/best-practice-security.html
[^15]: Node.js 26.7.0,AsyncLocalStorage: https://nodejs.org/api/async_context.html#class-asynclocalstorage
[^16]: Express,Health checks and graceful shutdown: https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html
[^17]: Node.js release policy: https://nodejs.org/en/about/previous-releases
[^18]: Node.js 24.19.0 archive: https://nodejs.org/en/download/archive/v24.19.0
[^19]: Express releases, including 5.2.1 and maintained 4.x releases: https://github.com/expressjs/express/releases
[^20]: Node.js 26.7.0, TypeScript support: https://nodejs.org/api/typescript.html
[^21]: Node.js 26.7.0, Node-API ABI stability: https://nodejs.org/api/n-api.html