Skip to content

Conversation

@manureja64
Copy link
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Currently, when an application shuts down:

By default, the server stops accepting new connections but keeps existing keep-alive connections open indefinitely, potentially hanging the shutdown process (e.g., preventing a Kubernetes pod from terminating).

If forceCloseConnections is set to true, the server destroys all open sockets immediately. This kills in-flight requests, causing clients to experience "socket hang up" or ECONNREFUSED errors.

Issue Number: #11416

What is the new behavior?

This PR adds a gracefulShutdown property to NestApplicationOptions.

When gracefulShutdown: true is enabled (currently implemented for the Express adapter):

Upon calling app.close(), a flag isShuttingDown is set to true.

A middleware intercepts any new requests (even those arriving on existing keep-alive connections) and immediately responds with 503 Service Unavailable and a Connection: close header.

In-flight requests are allowed to proceed and complete naturally.

This allows load balancers and clients to gracefully back off while ensuring no active work is interrupted.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

This implementation follows a pattern similar to Fastify's return503OnClosing option, providing a middle ground between hanging indefinitely and forcefully killing connections.

Verified with a new integration test suite in integration/graceful-shutdown.

Override root tsconfig.json exclusion so TypeScript project service finds
e2e specs. Also set target, strict, and
esModuleInterop compiler options.
Remove leftover debug logging statements to reduce console noise in the adapter.
@coveralls
Copy link

coveralls commented Jan 4, 2026

Pull Request Test Coverage Report for Build c8f84b05-bcfe-45ee-af7b-7f904ed3d19b

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 89.759%

Totals Coverage Status
Change from base Build a41a5432-6384-48b4-9662-8f7d90ec0de6: 0.0%
Covered Lines: 7441
Relevant Lines: 8290

💛 - Coveralls

@kamilmysliwiec
Copy link
Member

Correct me if i'm wrong but this feels like something that could be implemented outside of the framework's package

Move isShuttingDown flag activation from dispose() to a new
prepareClose() step that runs before callDestroyHook(). This prevents
new requests from being processed while providers are being destroyed
during shutdown.

Add beforeClose() to HttpServer interface and AbstractHttpAdapter so
adapters can be notified before the shutdown lifecycle begins.
@manureja64
Copy link
Contributor Author

Hi @kamilmysliwiec, thanks for the feedback!

You're right that a basic version of this (middleware + flag) can be
implemented externally. However, doing it correctly requires coordination with
the framework's internal shutdown lifecycle — which is hard to achieve from
userland.

The key challenge is timing. The shutdown sequence runs:

  1. callDestroyHook() → onModuleDestroy (providers start cleaning up)
  2. callBeforeShutdownHook() → beforeApplicationShutdown
  3. dispose() → httpAdapter.close()
  4. callShutdownHook() → onApplicationShutdown

If the "reject new requests" flag is set externally (e.g., via
beforeApplicationShutdown), it only kicks in at step 2 — but providers are
already being destroyed in step 1. Requests arriving during that window can
hit providers whose database connections, caches, or other dependencies have
already been torn down. This is the exact scenario described in #11416.

To fix this properly, the latest commit (a1b82bf) introduces a prepareClose()
hook in NestApplicationContext that runs before callDestroyHook(), and a
beforeClose() method on the HttpServer interface/AbstractHttpAdapter. This
ensures new requests are rejected before any provider teardown begins —
something an external implementation cannot guarantee without coupling to
framework internals.

Additionally, Fastify ships return503OnClosing as a built-in (enabled by
default). Adding Express parity here provides a consistent experience across
adapters.

Happy to discuss the approach further or adjust anything.

@kamilmysliwiec
Copy link
Member

WDYT about renaming this attribute to return503OnClosing then, instead of more vague gracefulShutdown (which might be confusing)

Rename gracefulShutdown to return503OnClosing to align the HTTP
option name with Fastify's terminology. The previous name was too
vague and could imply broader behavior (connection draining, grace
periods) beyond what it actually does.
@manureja64
Copy link
Contributor Author

Hi @kamilmysliwiec, your feedback makes sense to me. I have made the required changes. Please have a look.

@manureja64
Copy link
Contributor Author

Hi @kamilmysliwiec, hope you're doing well! I've implemented the rename from gracefulShutdown to return503OnClosing as you suggested — the latest commits reflect that change.

Just wanted to check in: are there any other changes or adjustments you'd like me to make? Happy to iterate further if needed. Looking forward to your feedback review when you get a chance. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants