Which @angular/* package(s) are the source of the bug?
@angular/build
Is this a regression?
Yes — introduced in 21.2.14 by commit `d46c082fb` ("fix(@angular/build): prevent esbuild service child process leakage")
Description
`ng build --configuration=production` deadlocks permanently inside Docker on Linux after 8–20 minutes. No error, no output, process never exits. This breaks every standard Docker-based CI/CD pipeline.
Root cause — confirmed by source diff between 21.2.12 and 21.2.14:
`src/tools/esbuild/bundler-context.js`, non-incremental build path changed from:
// 21.2.12 — correct for production: one-shot, no persistent process
result = await esbuild_1.build(this.#esbuildOptions);
to:
// 21.2.14+ — wrong for production: persistent child process + IPC pipe
this.#esbuildContext = await esbuild_1.context(this.#esbuildOptions);
result = await this.#esbuildContext.rebuild();
Why this deadlocks:
`esbuild.context()` spawns a persistent child process communicating via stdin/stdout IPC pipe. Angular's TypeScript compiler uses worker threads that call `Atomics.wait()` on a SharedArrayBuffer, blocking until the main thread signals completion. The main thread awaits the esbuild IPC response. Inside Docker on Linux, pipe buffering behaviour differs from the host — the esbuild child blocks writing to a pipe that nobody reads. Circular deadlock. All threads sleep permanently.
Observed state during deadlock:
- All 11 Node.js worker threads: `State: S`, `wchan: 0` (userspace `Atomics.wait()`)
- esbuild child process: 0.4% CPU (idle ping only)
- CPU ticks delta over 5 seconds: 0 — confirmed deadlock, not a slow build
Why this change is wrong for production builds:
The commit message states the leakage "primarily impacts programmatic usage and custom Architect decorators, where the hosting Node.js process remains alive after build completion."
That is precisely not what `ng build` is. A production build runs once and exits. There is nothing to leak. `esbuild.build()` was always correct for non-incremental builds.
Switching non-incremental production builds to `esbuild.context()` + `.rebuild()` to fix a leakage that doesn't exist in standard `ng build` usage introduced a critical regression for Docker/CI environments — which is the majority of production Angular build infrastructure.
Impact:
Corporate and enterprise environments run Angular builds inside Docker containers in CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, Kubernetes build jobs). These environments have standard resource allocations. They cannot and should not need to provision extra CPU and RAM to work around a pipe IPC timing issue introduced by an unnecessary API switch for production builds.
This is a silent failure: no error message, no log output, the build simply hangs. Teams debugging this lose hours with no indication of the cause.
Proposed fix:
Restore `esbuild.build()` for the non-incremental branch in `src/tools/esbuild/bundler-context.js`. The scoped `esbuild.context()` should only be used when `this.incremental` is true (watch mode), which is exactly what 21.2.12 and 21.2.13 did correctly:
if (this.#esbuildContext) {
// Rebuild using the existing incremental build context
result = await this.#esbuildContext.rebuild();
}
else if (this.incremental) {
// Create an incremental build context and perform the first build.
this.#esbuildContext = await esbuild_1.context(this.#esbuildOptions);
result = await this.#esbuildContext.rebuild();
}
else {
// For non-incremental builds, perform a single build — esbuild.build() is correct here.
// esbuild.context() creates a persistent IPC pipe that deadlocks in Docker on Linux.
result = await esbuild_1.build(this.#esbuildOptions);
}
If the child process leakage needs fixing for non-incremental builds too, it should be addressed within the `esbuild.build()` path (e.g. explicit service stop after completion) — not by switching to `esbuild.context()` which introduces IPC timing sensitivity.
Request:
Either revert the non-incremental build path to `esbuild.build()`, or provide a technical justification for why `esbuild.context()` is necessary for non-incremental production builds and how Docker environments are expected to handle the resulting IPC deadlock.
Environment
Node: 22.x
Docker: Linux amd64 (node:22-slim)
@angular/build: 21.2.14 through 21.2.17 (all affected)
Confirmed working: @angular/build 21.2.12 and 21.2.13
OS inside container: Debian (slim) / Alpine
Which @angular/* package(s) are the source of the bug?
@angular/build
Is this a regression?
Yes — introduced in 21.2.14 by commit `d46c082fb` ("fix(@angular/build): prevent esbuild service child process leakage")
Description
`ng build --configuration=production` deadlocks permanently inside Docker on Linux after 8–20 minutes. No error, no output, process never exits. This breaks every standard Docker-based CI/CD pipeline.
Root cause — confirmed by source diff between 21.2.12 and 21.2.14:
`src/tools/esbuild/bundler-context.js`, non-incremental build path changed from:
to:
Why this deadlocks:
`esbuild.context()` spawns a persistent child process communicating via stdin/stdout IPC pipe. Angular's TypeScript compiler uses worker threads that call `Atomics.wait()` on a SharedArrayBuffer, blocking until the main thread signals completion. The main thread awaits the esbuild IPC response. Inside Docker on Linux, pipe buffering behaviour differs from the host — the esbuild child blocks writing to a pipe that nobody reads. Circular deadlock. All threads sleep permanently.
Observed state during deadlock:
Why this change is wrong for production builds:
The commit message states the leakage "primarily impacts programmatic usage and custom Architect decorators, where the hosting Node.js process remains alive after build completion."
That is precisely not what `ng build` is. A production build runs once and exits. There is nothing to leak. `esbuild.build()` was always correct for non-incremental builds.
Switching non-incremental production builds to `esbuild.context()` + `.rebuild()` to fix a leakage that doesn't exist in standard `ng build` usage introduced a critical regression for Docker/CI environments — which is the majority of production Angular build infrastructure.
Impact:
Corporate and enterprise environments run Angular builds inside Docker containers in CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, Kubernetes build jobs). These environments have standard resource allocations. They cannot and should not need to provision extra CPU and RAM to work around a pipe IPC timing issue introduced by an unnecessary API switch for production builds.
This is a silent failure: no error message, no log output, the build simply hangs. Teams debugging this lose hours with no indication of the cause.
Proposed fix:
Restore `esbuild.build()` for the non-incremental branch in `src/tools/esbuild/bundler-context.js`. The scoped `esbuild.context()` should only be used when `this.incremental` is true (watch mode), which is exactly what 21.2.12 and 21.2.13 did correctly:
If the child process leakage needs fixing for non-incremental builds too, it should be addressed within the `esbuild.build()` path (e.g. explicit service stop after completion) — not by switching to `esbuild.context()` which introduces IPC timing sensitivity.
Request:
Either revert the non-incremental build path to `esbuild.build()`, or provide a technical justification for why `esbuild.context()` is necessary for non-incremental production builds and how Docker environments are expected to handle the resulting IPC deadlock.
Environment