Add opt-in graceful shutdown for active SSE contexts
- Dominant language
- JavaScript
- Stars
- 28
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
### Prerequisites
- [x] I have written a descriptive issue title
- [x] I have searched existing issues to ensure the feature has not already been requested
### 🚀 Feature Proposal
Add an opt-in plugin option that gracefully closes SSE contexts when Fastify begins shutting down.
```js
await fastify.register(fastifySSE, {
closeOnShutdown: true
})
```
The option can default to false for backward compatibility. When enabled, it would apply to every active SSEContext, regardless of whether `reply.sse.keepAlive()` was called.
### Motivation
An active SSE response can prevent `fastify.close()` from resolving because Fastify waits for in-flight responses to complete. This affects both of the following producer styles.
#### Out-of-band producer using keepAlive()
```js
fastify.get('/events', { sse: 'only' }, async (_request, reply) => {
reply.sse.keepAlive()
await reply.sse.send({ data: 'connected' })
const interval = setInterval(() => {
void reply.sse.send({ data: 'update' })
}, 1000)
reply.sse.onClose(() => {
clearInterval(interval)
})
})
```
#### Awaited long-running producer without keepAlive()
```js
async function * events () {
while (true) {
yield await eventQueue.next()
}
}
fastify.get('/events', { sse: 'only' }, async (_request, reply) => {
await reply.sse.send(events())
})
```
The second example should not call keepAlive(): the pending handler already keeps the response active. Nevertheless, it can block shutdown just like an out-of-band keep-alive context. Targeting only contexts that called keepAlive() was considered but leaves a gap.
Note that passing in abort signals, as suggested in !56 can then help make the second example break instantly instead of letting the generator break when `eventQueue.next()` returns.
### Why not `forceCloseConnections: true`?
Fastify provides `forceCloseConnections: true`, but that destroys all persistent connections without determining whether their requests have
completed so it affects more than SSE and is not a "graceful shutdown".
This proposal would be narrower:
- Only SSE contexts owned by this plugin are affected.
- Committed responses end through the existing `SSEContext.close()` path.
- Sockets are not explicitly destroyed.
- Heartbeat cleanup runs normally.
- `reply.sse.onClose()` callbacks run normally.
- Unrelated Fastify responses continue draining.
### Proposed behavior
When `closeOnShutdown` is true:
- Maintain a registry of active `SSEContext` instances attached via `reply.sse`.
- Remove it through the context’s normal cleanup path.
- Install a Fastify preClose hook.
- Close all SSE contexts belonging to requests that remain active during shutdown.
### Example
Calling `app.close()` triggers the plugin’s `preClose` hook, which closes active SSE contexts. That runs `onClose()`, clears the interval, ends the response gracefully, and allows shutdown to complete. With the current implementation against Fastify's main branch, this example causes Fastify to wait forever before shutting down.
```js
const Fastify = require('fastify')
const fastifySSE = require('@fastify/sse')
const app = Fastify()
await app.register(fastifySSE, {
closeOnShutdown: true
})
app.get('/events', { sse: 'only' }, async (_request, reply) => {
reply.sse.keepAlive()
await reply.sse.send({ data: 'connected' })
const interval = setInterval(() => {
void reply.sse.send({ data: 'update' })
}, 1000)
reply.sse.onClose(() => {
clearInterval(interval)
})
})
await app.listen({ port: 3000 })
process.once('SIGTERM', async () => {
await app.close()
})
```
---
Note that I want to submit a PR for this and am posting this so we can discuss some details:
- Is `closeOnShutdown` an appropriate name?
- I think defaulting to false makes sense to prevent making this a breaking change even if it's technically an improvement, but I'm curious to know what you think.
- I think closing all contexts regardless of `keepAlive()` makes sense because of the examples I provided, but maybe I'm missing something
- Should shutdown close active contexts immediately like proposed, or only notify producers and wait for them to close cooperatively? Cooperative notification preserves producer-controlled draining but then we can't guarantee that `fastify.close()` completes without an additional deadline or forced-close policy.
Contributor guide
Research direction
Start at the plugin registration, the SSEContext lifecycle, and Fastify's app.close() path described in the issue; trace how reply.sse contexts are created and normally cleaned up. Done means an opt-in closeOnShutdown option registers a preClose hook that closes active SSE contexts through their normal path, while the default remains unchanged and the stated producer styles are covered by tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100