Docs

Engineering · Measurements

Server Footprint

Most servers import about 25 libraries and carry about 100. This page records bundle size, heap and RSS for every server and library, what the heap is made of, and the result of merging the servers.

191 libraries13 servers and workersmeasured 2026-10-04
base develop @ c1a372e908 · tool tools/lib-footprint (uncommitted) · macOS arm64, Node 25

Baseline

Each server was bundled with rspack using the production externals and minifier. The entry is AppModule, so loading the bundle evaluates every module without booting Nest. Heap and RSS are deltas after a forced GC, median of three runs.

ServerBundleHeapRSSLibraries importedLibraries carried
tasks-server68.4 MB297 MB731 MB90143
api-server68.4 MB296 MB763 MB120148
sync-worker65.3 MB270 MB683 MB60131
public-server57.3 MB234 MB569 MB24113
scp-server55.2 MB245 MB575 MB58123
content-worker53.8 MB231 MB567 MB39120
platform-worker52.4 MB227 MB565 MB42106
relay-server52.0 MBnot measurednot measured22103
internal-server48.6 MB217 MB540 MB25106
website-worker47.9 MB210 MB540 MB21102
leads-server44.3 MB198 MB520 MB2691
auth-server31.8 MB134 MB342 MB1444
links-server11.6 MB59 MB180 MB812

The real nx run api-server:build-rspack output is 79 MB. It enters at main.ts, which adds instrumentation, so the harness is in the right range. relay-server needs a native WebRTC binary that does not exist on macOS, so it could not be loaded.

Remark. These are module-evaluation costs. A booted server adds provider instances, connection pools and caches on top. Production memory was not measured.

Heap composition

The api-server bundle was loaded under V8's sampling heap profiler. The profiler keeps only allocations that survive GC, and each one was mapped to its source file through the source map. Total: 297 MB.

What holds the memoryHeapShare
Bundle source text, kept by V8 for lazy compilation75 MB25%
Function and closure objects created while modules evaluate61 MB21%
npm package code83 MB28%
V8 internals and Node loader52 MB18%
Our library code24 MB8%
api-server app code2 MB1%

The 191 libraries allocate 24 MB between them. The rest is the cost of having the code loaded at all, plus what the npm packages build at import.

The npm packages that allocate most:

PackageHeapReaches the server through
zod20.2 MBagents (13.4 MB), mcp, Command Center tool libraries
class-validator11.6 MBevery DTO; scheduling is the largest user
@apimatic/schema7.3 MBsquare
googleapis5.7 MBauth
drizzle-orm5.7 MBdrizzle-orm schema library
reflect-metadata3.4 MBdecorator metadata
@linear/sdk2.5 MBlinear

By owner, meaning the nearest workspace library on the call stack when the memory was allocated:

OwnerHeapMostly
agents18.8 MBzod schemas
monitoring17.6 MBfirst importer of the tracing and Nest packages
auth15.2 MBgoogleapis
aws-utils15.0 MBAWS SDK clients
drizzle-orm12.9 MBtable definitions
square11.1 MB@apimatic/schema
platform-scraper8.1 MBfiles read at import
linear7.3 MB@linear/sdk
scheduling6.7 MBclass-validator metadata
zoca-utils6.5 MBmoment-timezone

Remark. An npm package's own start-up cost lands on the first library that imports it. monitoring is imported first by almost everything, so its figure includes packages that other libraries would otherwise carry.

links-server imports 8 libraries and carries 11. None of them leads into a domain library, so the chain stops at the foundation: config, cache, redis, logger, monitoring, drizzle-orm, database, bull, aws-utils, zoca-utils, links.

links-serverapi-server
Libraries carried11148
npm packages368571
Bundle11.6 MB68.4 MB
Heap59 MB296 MB
RSS180 MB763 MB

Its heap has the same shape as api-server's: a quarter is bundle source text and a quarter is function objects. Heap follows bundle size at roughly 4 to 5 MB per bundle MB on every server measured. links-server is the floor a server reaches when it imports only foundation libraries. That floor is about 60 MB of heap, and aws-utils (14.6 MB), bull (9.4 MB) and drizzle-orm (6.9 MB) are most of it.

Library shape

The 191 libraries have no import cycles between them. The weight comes from long chains.

Each library was bundled and loaded the same way as the servers. The figures are medians per tier.

Other libraries carriedLibraries in tierBundleHeapRSS
100 or more852.8 MB229 MB569 MB
60 to 993237.9 MB159 MB423 MB
30 to 592827.3 MB114 MB295 MB
10 to 294313.2 MB78 MB210 MB
under 10798.4 MB41 MB117 MB

Importing any one of the top 40 libraries costs a server at least 147 MB of heap. The foundation is not free either: bull alone is 9.1 MB and 47 MB of heap, and drizzle-orm is 5.3 MB and 37 MB.

The eight heaviest are the Command Center tool libraries and local-seo: command-center-agent/host, scheduling-agent-tools, discovery-agent-tools, frontdesk-agent-tools, support-agent-tools, local-seo/keyword-gen, local-seo/map-grid, local-seo/local-seo. The first libraries where the closure jumps are services (42), gbp/gbp-location (43) and gift-cards (40).

The 25 largest libraries and the shared foundation
LibraryBundleHeapRSSLoadLibraries carried
command-center-agent/host54.3 MB237 MB573 MB2.1 s117
scheduling-agent-tools54.0 MB237 MB569 MB2.2 s115
discovery-agent-tools53.6 MB234 MB562 MB2.2 s116
frontdesk-agent-tools52.8 MB229 MB572 MB2.1 s115
support-agent-tools52.7 MB228 MB571 MB2.1 s114
local-seo/keyword-gen52.0 MB224 MB562 MB2.1 s109
local-seo/map-grid51.8 MB224 MB558 MB2.1 s107
local-seo/local-seo51.8 MB223 MB564 MB2.1 s106
loyalty48.7 MB212 MB538 MB2.0 s98
frontdesk-agent48.6 MB212 MB535 MB2.1 s98
billing48.6 MB212 MB537 MB2.3 s98
win48.5 MB213 MB535 MB2.0 s97
website-backend47.4 MB208 MB530 MB1.9 s96
integrated-platforms45.7 MB200 MB518 MB1.8 s92
platform-migration44.5 MB197 MB515 MB1.8 s89
gateway44.3 MB198 MB516 MB1.8 s92
waitlist44.1 MB196 MB515 MB1.8 s89
scheduling44.1 MB197 MB517 MB1.8 s88
instantly41.7 MB160 MB432 MB1.6 s69
hubspot41.6 MB159 MB424 MB1.5 s68
website/website-creation38.1 MB159 MB422 MB1.6 s75
blogs38.1 MB160 MB425 MB1.6 s75
gbp/gbp-completion37.9 MB159 MB422 MB1.5 s76
local-seo/keywords-demand37.9 MB159 MB425 MB1.5 s75
local-seo/keywords-competitor37.8 MB157 MB419 MB1.5 s71

Shared foundation and hub libraries, for scale.

LibraryBundleHeapRSSLibraries carried
config0.7 MB3 MB14 MB0
monitoring4.1 MB17 MB60 MB1
zoca-utils6.2 MB24 MB76 MB2
drizzle-orm5.3 MB37 MB104 MB2
bull9.1 MB47 MB138 MB5
auth18.7 MB64 MB186 MB6
entity12.3 MB72 MB207 MB19
media27.0 MB113 MB291 MB37
services31.2 MB130 MB320 MB42

The full table is dist/lib-footprint/prod/footprint.csv after a tool run.

Causes

Contract leaks

A type, constant or pure function is imported through a heavy library's barrel. The import pulls in the whole library and everything behind it.

EdgeWhat crosses itLibraries shed from the importer
local-seo/keywords-demand → contentPaginatedDto67
local-seo/keywords-competitor → lead-magnetfour SQL query constants67
platform-migration → schedulingexpandRecurrenceRule, validateRecurrenceRule, one service45
scheduling → booking-agentcomputeRecommendation, renderVisitorBlock16 server inclusions
media → image-taggingconstant arrays in four of five files7 server inclusions

A later symbol-level analysis showed that a contracts library alone clears few edges, because most edges that import a constant also inject a service. See Library Layers.

Side effects called inline

The caller does not use the result. These are the Bull candidates.

EdgeCall
media → requestsone createFromTemplate
phone → commstwo un-awaited createCommsProviderContact
content → updatesone markContentApproved
packages → websitewebsiteUpdatedEvent
scheduling → chatbotdeactivateAllFollowUpsForClient
pipedrive → moengagecreateEvent, createUser
clients → active-campaignbulkImportContacts

Inline reads

The caller needs the answer before it continues. A queue does not help.

EdgeCall
chats → usergetUserData
user → phonegetByEntityId, create
gateway → schedulingisSchedulingActive, one boolean
prompts → entityentity preference reads and upserts
services → platform-scrapergetBusinessDetails

The googleapis meta-package

Eight files in six libraries import googleapis, including libs/auth. It lands in 12 of 13 servers. Bundled alone it is 9.7 MB minified, 27 MB of heap and 187 ms to load. We use calendar, webmasters, site verification, indexing and auth. @googleapis/calendar is already a dependency and costs 0.5 MB and 4.8 MB of heap.

ES5 output

Nx's rspack plugin sets no swc target, so our code ships as ES5. The real api-server bundle contains 15,521 generator state machines. An ES2022 target makes the large servers 16–19% smaller. It cannot be enabled today: 10 of the 12 loadable servers crash on circular imports that ES5 hoisting hides. ChatbotService and PostService are the two traced so far.

Plan

The plan is in Codebase Organization Plan. This page holds the measurements behind it.

Merged server

Seven HTTP servers collapse into two processes. apps/unified-server holds the five authenticated servers. apps/unified-public-server holds the customer-facing ones. Workers are not included. Both are evaluation builds and are not committed.

ProcessContainsURL prefix
unified-serverapi-servernone
tasks-servertasks/api/v1
scp-serverscp/api/v1
auth-serverauth/api/v1
internal-serverinternal/api/v1
unified-public-serverpublic-servernone
leads-servernone

No URL changes. Each prefix is the one that server's setGlobalPrefix already used. links-server is not mounted because public-server already serves all five of its paths.

Every server was built with its production rspack config, booted against the local Postgres and Redis, and sampled after it settled. Startup migrations and outbound calls were switched off.

ProcessBundleHeapSeparate servers it replacesTheir heap
unified-server85.5 MB433 MBapi, tasks, scp, auth, internal1,352 MB
unified-public-server63.5 MB252 MBpublic, leads458 MB
Both149.0 MB685 MBseven servers1,810 MB

Per replicaEvery replica of a merged process carries its full heap. The saving shrinks as replica counts rise.

Equivalence check

Each standalone server and each merged server was booted, and every parameterless GET route was requested without credentials. Status code and JSON shape were compared route by route.

unified-serverunified-public-server
Routes, standalone servers3,367158
Routes, merged3,367158
Routes lost or added00
GET routes probed58436
Status code differs00
Response shape differs01

The one difference is /health in the public process. public-server's health controller answers it, so the body has public-server's fields and not leads-server's.

The check covers unauthenticated GET requests only. Authenticated requests, writes, WebSocket gateways, crons and queue processors were not exercised.

How the merge works

mountServer builds a stand-in for each server's root module from its metadata. Three things needed handling.

Global enhancers
A guard, interceptor or filter registered with APP_GUARD, APP_INTERCEPTOR or APP_FILTER is global to the whole process. Each one is wrapped so it acts only on its own server's controllers. Without this, leads-server's AuthGuard and response wrapper would apply to public-server's open routes.
Shared controller modules
Nest creates a module once, so its controllers get one prefix. ContentModule is imported by api, tasks and scp. MapGridModule is imported by tasks and scp. Their controllers are re-declared under each later server's prefix.
Dynamic modules
McpModule.registerServer carries its controllers and imports on the registration object. The module walker reads both.

Open problems

  1. links-server's /r/:alias caches the link in Redis and sends Cache-Control: no-store. public-server's handler for the same path does neither. Dropping links-server changes that behaviour.
  2. Re-mounting widens the exports of ContentModule and MapGridModule so the re-declared controllers can resolve their providers. This is a runtime metadata change made by the merge helper.
  3. The merged servers each use one validation pipe and one body limit. The pipe options are identical within each group. The body limit is 50 MB, the largest any server set.
  4. Lint. Both apps import other apps by relative path, which @nx/enforce-module-boundaries rejects. Each server's root module would need to move into a library.
  5. No unit tests for the merge helper.
  6. One event loop and one heap limit per merged process.

links-server fails to boot on its own from current develop with a Nest dependency-resolution error.

Method and limits

Bundles
One rspack bundle per library entry point and per server AppModule. Externals, aliases and swc options match apps/api-server/rspack.config.js.
Memory
Each bundle is loaded in its own Node process with no application environment. Nothing connects to a database, Redis or a third-party API.
Heap profile
heap.js runs the bundle under --heap-prof and maps allocation sites through the source map. It reports who allocated and which workspace library was on the stack.
Graph
Import statements resolved through tsconfig.base.json paths. import type is excluded because it is erased at build time.
Not covered
Booted runtime memory, production RSS, relay-server and remote-browser. The last two need a native WebRTC binary that does not exist on macOS. 190 of 191 libraries have an entry point and were bundled.