elysia-graceful-shutdown
v0.1.0
Published
Graceful shutdown plugin for Elysia.
Downloads
407
Readme
elysia-graceful-shutdown
Graceful shutdown plugin for Elysia.
This plugin helps your Elysia app shut down in a predictable way when it receives termination signals such as SIGTERM or SIGINT.
It provides:
- signal handling
- shutdown lifecycle hooks
- in-flight request draining before cleanup
Table of Contents
Installation
bun add elysia-graceful-shutdownUsage
import { Elysia } from 'elysia';
import { gracefulShutdown } from 'elysia-graceful-shutdown';
const app = new Elysia()
.use(
gracefulShutdown({
signals: ['SIGTERM', 'SIGINT'],
timeout: 30_000,
drainTimeout: 10_000,
preShutdown: ({ activeRequestCount }) => {
console.log('shutdown begin', { activeRequestCount });
},
onShutdown: async ({ activeRequestCount }) => {
console.log('all in-flight requests finished', { activeRequestCount });
await db.destroy();
},
finally: async ({ signal, state, activeRequestCount }) => {
console.log('shutdown finished', { signal, state, activeRequestCount });
},
}),
)
.get('/', () => 'hello');
app.listen(3000);Shutdown Flow
sequenceDiagram
participant P as Parent Process
participant S as Elysia Server
participant C as Client
Note over S,C: (1) Server is running normally
C->>S: Request
S-->>C: Response
P->>S: (2) SIGINT / SIGTERM
Note over S: shutdown initiated
S->>S: (3) preShutdown()
Note over S: state changes to shutting_down
Note over S: (4) reject new incoming requests
C->>S: Request during shutdown
S-->>C: Rejected / unavailable
Note over S: (5) wait for in-flight requests to finish
Note over S: continue after drainTimeout if requests are still running
S->>S: (6) onShutdown()
S->>S: (7) app.stop()
Note over S: force app.stop(true) after timeout
S->>S: (8) finally()
Note over S: (9) process terminates naturallyThe shutdown hooks receive activeRequestCount, which reflects the number of
tracked in-flight HTTP requests at that phase of the shutdown flow.
For runnable demos, see example/request-drain.ts and example/request-timeout.ts.
Options
signals
Signals that trigger the shutdown flow.
Deafult:
['SIGTERM', 'SIGINT'];timeout
Maximum time to allow the whole signal-driven shutdown flow to finish before
the plugin force-stops the app with app.stop(true).
This is a shutdown deadline for the server lifecycle, not a guarantee that all already-running user-land work will be interrupted.
This timeout covers:
preShutdown(context)- request draining
onShutdown(context)app.stop()
Value is in milliseconds.
Default:
30_000; // 30 secondsnew Elysia().use(
gracefulShutdown({
timeout: 30_000,
}),
);What this option guarantees:
- the plugin stops accepting new work through the normal shutdown flow
- the plugin escalates from
app.stop()toapp.stop(true)when the deadline is exceeded - active connections are force-terminated at the Bun server level
What this option does not guarantee:
- arbitrary user-land handler code is preempted immediately
- long-running async work such as
sleep(), database calls, or external API work is cancelled automatically
On Bun, app.stop(true) immediately terminates active connections, but a
long-running async handler may still finish its own work after the client
connection has been closed.
drainTimeout
Maximum time to wait for tracked in-flight HTTP requests to drain before the shutdown flow continues.
This is narrower than timeout: drainTimeout only limits the request-drain
phase, while timeout limits the whole signal-driven shutdown path.
In other words:
drainTimeoutcontrols how long the plugin waits for tracked requests before moving ontimeoutcontrols when the plugin stops waiting for the overall shutdown lifecycle and escalates toapp.stop(true)
Constraint:
drainTimeoutmust be less than or equal totimeout- if you omit
timeout, the default30_000still applies to this rule
The plugin validates this at startup and throws if drainTimeout is greater
than the effective timeout, because otherwise the broader shutdown deadline
would expire before the request-drain budget could ever be used fully.
Value is in milliseconds.
Default:
30_000; // 30 secondsnew Elysia().use(
gracefulShutdown({
drainTimeout: 30_000,
}),
);preShutdown(context)
Runs at the beginning of the shutdown flow.
Use this when you need to perform very early shutdown work before the plugin waits for tracked in-flight requests to drain and before the main cleanup phase.
Examples:
- marming internal state as shutting down
- stopping schedulers
- preparing the app for shutdown
- loggin shutdown start
new Elysia().use(
gracefulShutdown({
preShutdown: (context) => {
console.log('Shutdown begin');
console.log('Shutdown Signal: ', context.signal);
},
}),
);onShutdown(context)
Runs during the main cleanup phase, after tracked in-flight requests have
finished or after the configured drainTimeout has elapsed.
Use this for resource cleanup such as:
- Closing database connections
- Disconnecting Redis
- Stoping queue consumeer
- Shutting down background workers
new Elysia().use(
gracefulShutdown({
onShutdown: async (context) => {
console.log('Clean up');
await datasource.destroy();
await eventStore.end();
},
}),
);onError(context)
Runs when the plugin catches an error during signal-driven shutdown.
Use this to forward shutdown failures to your application's logger or observability pipeline instead of letting the plugin write directly to stderr.
The phase field is one of:
shutdown: a lifecycle hook failedstop:app.stop()failedtimeout: the overall shutdown deadline elapsed and the plugin escalated toapp.stop(true)
For phase: 'timeout', this means the plugin hit the server shutdown deadline.
It does not necessarily mean every already-running handler was synchronously
aborted in user land.
new Elysia().use(
gracefulShutdown({
onError: ({ phase, error, signal }) => {
logger.error('graceful shutdown failed', {
phase,
signal,
error,
});
},
}),
);finally(context)
Runs at the end of the shutdown flow.
Use this for short final work such as:
- final logging
- metrics markers
- lightweight finalization
new Elysia().use(
gracefulShutdown({
finally: async ({ signal, state }) => {
console.log('Shutdown finished');
console.log('Shutdown state: ', state);
console.log('Shutdown Signal: ', signal);
},
}),
);