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.
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.
| Server | Bundle | Heap | RSS | Libraries imported | Libraries carried |
|---|---|---|---|---|---|
| tasks-server | 68.4 MB | 297 MB | 731 MB | 90 | 143 |
| api-server | 68.4 MB | 296 MB | 763 MB | 120 | 148 |
| sync-worker | 65.3 MB | 270 MB | 683 MB | 60 | 131 |
| public-server | 57.3 MB | 234 MB | 569 MB | 24 | 113 |
| scp-server | 55.2 MB | 245 MB | 575 MB | 58 | 123 |
| content-worker | 53.8 MB | 231 MB | 567 MB | 39 | 120 |
| platform-worker | 52.4 MB | 227 MB | 565 MB | 42 | 106 |
| relay-server | 52.0 MB | not measured | not measured | 22 | 103 |
| internal-server | 48.6 MB | 217 MB | 540 MB | 25 | 106 |
| website-worker | 47.9 MB | 210 MB | 540 MB | 21 | 102 |
| leads-server | 44.3 MB | 198 MB | 520 MB | 26 | 91 |
| auth-server | 31.8 MB | 134 MB | 342 MB | 14 | 44 |
| links-server | 11.6 MB | 59 MB | 180 MB | 8 | 12 |
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 memory | Heap | Share |
|---|---|---|
| Bundle source text, kept by V8 for lazy compilation | 75 MB | 25% |
| Function and closure objects created while modules evaluate | 61 MB | 21% |
| npm package code | 83 MB | 28% |
| V8 internals and Node loader | 52 MB | 18% |
| Our library code | 24 MB | 8% |
| api-server app code | 2 MB | 1% |
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:
| Package | Heap | Reaches the server through |
|---|---|---|
zod | 20.2 MB | agents (13.4 MB), mcp, Command Center tool libraries |
class-validator | 11.6 MB | every DTO; scheduling is the largest user |
@apimatic/schema | 7.3 MB | square |
googleapis | 5.7 MB | auth |
drizzle-orm | 5.7 MB | drizzle-orm schema library |
reflect-metadata | 3.4 MB | decorator metadata |
@linear/sdk | 2.5 MB | linear |
By owner, meaning the nearest workspace library on the call stack when the memory was allocated:
| Owner | Heap | Mostly |
|---|---|---|
agents | 18.8 MB | zod schemas |
monitoring | 17.6 MB | first importer of the tracing and Nest packages |
auth | 15.2 MB | googleapis |
aws-utils | 15.0 MB | AWS SDK clients |
drizzle-orm | 12.9 MB | table definitions |
square | 11.1 MB | @apimatic/schema |
platform-scraper | 8.1 MB | files read at import |
linear | 7.3 MB | @linear/sdk |
scheduling | 6.7 MB | class-validator metadata |
zoca-utils | 6.5 MB | moment-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.
Why links-server is small
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-server | api-server | |
|---|---|---|
| Libraries carried | 11 | 148 |
| npm packages | 368 | 571 |
| Bundle | 11.6 MB | 68.4 MB |
| Heap | 59 MB | 296 MB |
| RSS | 180 MB | 763 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 carried | Libraries in tier | Bundle | Heap | RSS |
|---|---|---|---|---|
| 100 or more | 8 | 52.8 MB | 229 MB | 569 MB |
| 60 to 99 | 32 | 37.9 MB | 159 MB | 423 MB |
| 30 to 59 | 28 | 27.3 MB | 114 MB | 295 MB |
| 10 to 29 | 43 | 13.2 MB | 78 MB | 210 MB |
| under 10 | 79 | 8.4 MB | 41 MB | 117 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
| Library | Bundle | Heap | RSS | Load | Libraries carried |
|---|---|---|---|---|---|
command-center-agent/host | 54.3 MB | 237 MB | 573 MB | 2.1 s | 117 |
scheduling-agent-tools | 54.0 MB | 237 MB | 569 MB | 2.2 s | 115 |
discovery-agent-tools | 53.6 MB | 234 MB | 562 MB | 2.2 s | 116 |
frontdesk-agent-tools | 52.8 MB | 229 MB | 572 MB | 2.1 s | 115 |
support-agent-tools | 52.7 MB | 228 MB | 571 MB | 2.1 s | 114 |
local-seo/keyword-gen | 52.0 MB | 224 MB | 562 MB | 2.1 s | 109 |
local-seo/map-grid | 51.8 MB | 224 MB | 558 MB | 2.1 s | 107 |
local-seo/local-seo | 51.8 MB | 223 MB | 564 MB | 2.1 s | 106 |
loyalty | 48.7 MB | 212 MB | 538 MB | 2.0 s | 98 |
frontdesk-agent | 48.6 MB | 212 MB | 535 MB | 2.1 s | 98 |
billing | 48.6 MB | 212 MB | 537 MB | 2.3 s | 98 |
win | 48.5 MB | 213 MB | 535 MB | 2.0 s | 97 |
website-backend | 47.4 MB | 208 MB | 530 MB | 1.9 s | 96 |
integrated-platforms | 45.7 MB | 200 MB | 518 MB | 1.8 s | 92 |
platform-migration | 44.5 MB | 197 MB | 515 MB | 1.8 s | 89 |
gateway | 44.3 MB | 198 MB | 516 MB | 1.8 s | 92 |
waitlist | 44.1 MB | 196 MB | 515 MB | 1.8 s | 89 |
scheduling | 44.1 MB | 197 MB | 517 MB | 1.8 s | 88 |
instantly | 41.7 MB | 160 MB | 432 MB | 1.6 s | 69 |
hubspot | 41.6 MB | 159 MB | 424 MB | 1.5 s | 68 |
website/website-creation | 38.1 MB | 159 MB | 422 MB | 1.6 s | 75 |
blogs | 38.1 MB | 160 MB | 425 MB | 1.6 s | 75 |
gbp/gbp-completion | 37.9 MB | 159 MB | 422 MB | 1.5 s | 76 |
local-seo/keywords-demand | 37.9 MB | 159 MB | 425 MB | 1.5 s | 75 |
local-seo/keywords-competitor | 37.8 MB | 157 MB | 419 MB | 1.5 s | 71 |
Shared foundation and hub libraries, for scale.
| Library | Bundle | Heap | RSS | Libraries carried |
|---|---|---|---|---|
config | 0.7 MB | 3 MB | 14 MB | 0 |
monitoring | 4.1 MB | 17 MB | 60 MB | 1 |
zoca-utils | 6.2 MB | 24 MB | 76 MB | 2 |
drizzle-orm | 5.3 MB | 37 MB | 104 MB | 2 |
bull | 9.1 MB | 47 MB | 138 MB | 5 |
auth | 18.7 MB | 64 MB | 186 MB | 6 |
entity | 12.3 MB | 72 MB | 207 MB | 19 |
media | 27.0 MB | 113 MB | 291 MB | 37 |
services | 31.2 MB | 130 MB | 320 MB | 42 |
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.
| Edge | What crosses it | Libraries shed from the importer |
|---|---|---|
local-seo/keywords-demand → content | PaginatedDto | 67 |
local-seo/keywords-competitor → lead-magnet | four SQL query constants | 67 |
platform-migration → scheduling | expandRecurrenceRule, validateRecurrenceRule, one service | 45 |
scheduling → booking-agent | computeRecommendation, renderVisitorBlock | 16 server inclusions |
media → image-tagging | constant arrays in four of five files | 7 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.
| Edge | Call |
|---|---|
media → requests | one createFromTemplate |
phone → comms | two un-awaited createCommsProviderContact |
content → updates | one markContentApproved |
packages → website | websiteUpdatedEvent |
scheduling → chatbot | deactivateAllFollowUpsForClient |
pipedrive → moengage | createEvent, createUser |
clients → active-campaign | bulkImportContacts |
Inline reads
The caller needs the answer before it continues. A queue does not help.
| Edge | Call |
|---|---|
chats → user | getUserData |
user → phone | getByEntityId, create |
gateway → scheduling | isSchedulingActive, one boolean |
prompts → entity | entity preference reads and upserts |
services → platform-scraper | getBusinessDetails |
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.
| Process | Contains | URL prefix |
|---|---|---|
| unified-server | api-server | none |
| tasks-server | tasks/api/v1 | |
| scp-server | scp/api/v1 | |
| auth-server | auth/api/v1 | |
| internal-server | internal/api/v1 | |
| unified-public-server | public-server | none |
| leads-server | none |
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.
| Process | Bundle | Heap | Separate servers it replaces | Their heap |
|---|---|---|---|---|
| unified-server | 85.5 MB | 433 MB | api, tasks, scp, auth, internal | 1,352 MB |
| unified-public-server | 63.5 MB | 252 MB | public, leads | 458 MB |
| Both | 149.0 MB | 685 MB | seven servers | 1,810 MB |
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-server | unified-public-server | |
|---|---|---|
| Routes, standalone servers | 3,367 | 158 |
| Routes, merged | 3,367 | 158 |
| Routes lost or added | 0 | 0 |
| GET routes probed | 584 | 36 |
| Status code differs | 0 | 0 |
| Response shape differs | 0 | 1 |
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.
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.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.McpModule.registerServer carries its controllers and imports on the registration object. The module walker reads both.Open problems
- links-server's
/r/:aliascaches the link in Redis and sendsCache-Control: no-store. public-server's handler for the same path does neither. Dropping links-server changes that behaviour. - Re-mounting widens the exports of
ContentModuleandMapGridModuleso the re-declared controllers can resolve their providers. This is a runtime metadata change made by the merge helper. - 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.
- Lint. Both apps import other apps by relative path, which
@nx/enforce-module-boundariesrejects. Each server's root module would need to move into a library. - No unit tests for the merge helper.
- 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
AppModule. Externals, aliases and swc options match apps/api-server/rspack.config.js.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.tsconfig.base.json paths. import type is excluded because it is erased at build time.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.