Shutdown
This page explains shutdown in the Vix WebSocket module.
Use it when you want to stop a WebSocket server safely, close active sessions, avoid blocking inside callbacks, and coordinate shutdown with an attached HTTP app.
Header
#include <vix/websocket/server.hpp>
#include <vix/websocket/AttachedRuntime.hpp>Or use the umbrella header:
#include <vix/websocket.hpp>Why shutdown matters
A WebSocket server keeps long-lived connections open.
That means shutdown must handle:
- active WebSocket sessions
- pending reads
- pending writes
- heartbeat loops
- I/O worker threads
- long-polling buffers
- shared runtime executor
- HTTP and WebSocket lifecycle coordination
The module separates non-blocking shutdown requests from final blocking cleanup.
Basic rule
The most important rule is:
Use stop_async() when shutdown must not block.
Use stop() when final cleanup is allowed to block.In attached HTTP + WebSocket applications:
HTTP shutdown callback
-> request non-blocking WebSocket stop
safe finalization path
-> perform blocking WebSocket stop
-> stop shared executorMain shutdown APIs
| API | Meaning |
|---|---|
Server::stop_async() | Request non-blocking WebSocket shutdown. |
Server::stop() | Stop the server and join internal threads. |
Session::close(...) | Close one session normally. |
Session::shutdown_now() | Force immediate session shutdown. |
AttachedRuntime::request_stop() | Request non-blocking stop for attached runtime. |
AttachedRuntime::finalize_shutdown() | Final blocking shutdown for attached runtime. |
Server shutdown model
vix::websocket::Server provides two shutdown paths:
ws.stop_async();
ws.stop();They are intentionally different.
stop_async
Use stop_async() when shutdown must return quickly.
ws.stop_async();This function:
- collects live sessions
- requests immediate shutdown for each live session
- requests low-level server shutdown
- does not wait for all I/O threads to finish
Conceptually:
Server::stop_async
-> collect live sessions
-> session.shutdown_now()
-> LowLevelServer::stop_async()
-> returnUse this from callbacks or signal paths where blocking is unsafe.
stop
Use stop() when you want final blocking cleanup.
ws.stop();This function stops the WebSocket server and joins internal server threads.
Conceptually:
Server::stop
-> stop_async
-> LowLevelServer::join_threadsUse it from a safe control path.
Do not call it from a callback that runs inside the WebSocket or HTTP server internals.
Session close
Use Session::close(...) for normal application-level disconnection.
session.close();With a reason:
session.close("normal shutdown");Use this when:
- one client should be disconnected
- a user logs out
- a protocol rule is violated
- the session is no longer authorized
- the application wants to close a single connection
Session immediate shutdown
Use Session::shutdown_now() for server shutdown paths.
session.shutdown_now();This is stronger than close(...).
It is intended for cases where the server must not depend on async close work being scheduled later.
Normal application logic should prefer:
session.close();Standalone server shutdown
For a standalone WebSocket server:
#include <memory>
#include <string>
#include <vix/config/Config.hpp>
#include <vix/executor/RuntimeExecutor.hpp>
#include <vix/websocket.hpp>
int main()
{
vix::config::Config config{".env"};
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);
vix::websocket::Server ws{config, executor};
ws.on_message(
[](vix::websocket::Session &session, const std::string &message)
{
session.send_text("echo: " + message);
});
ws.start();
ws.stop();
executor->stop();
return 0;
}In real applications, shutdown is usually triggered by a signal handler, control route, or application lifecycle manager.
Attached runtime shutdown
AttachedRuntime coordinates shutdown between:
vix::App
vix::websocket::Server
RuntimeExecutorThe attached runtime exists because HTTP and WebSocket often run together but must not block each other during shutdown.
AttachedRuntime request_stop
Use request_stop() to request a non-blocking WebSocket stop.
runtime.request_stop();This is safe to call from shutdown callbacks.
Internally, it calls:
ws.stop_async();AttachedRuntime finalize_shutdown
Use finalize_shutdown() for final blocking cleanup.
runtime.finalize_shutdown();Finalization order:
1. ws.stop()
2. executor.stop()This function is idempotent.
It is safe to call more than once.
Attached runtime shutdown flow
The attached shutdown flow is:
HTTP app shutdown requested
-> HTTP shutdown callback runs
-> AttachedRuntime requests ws.stop_async()
-> callback returns quickly
Later, from safe control path:
-> AttachedRuntime::finalize_shutdown()
-> ws.stop()
-> executor.stop()This avoids deadlocks and avoids blocking inside HTTP shutdown callbacks.
Why not block in shutdown callbacks
Shutdown callbacks may run inside an internal server thread or a path that must return quickly.
Blocking inside such a callback can cause:
- deadlocks
- stuck server shutdown
- thread join from the wrong thread
- partially stopped runtime state
- callbacks waiting on themselves
That is why the module separates:
non-blocking stop requestfrom:
blocking final cleanupMinimal attached runtime example
#include <memory>
#include <string>
#include <vix.hpp>
#include <vix/websocket.hpp>
int main()
{
vix::App app;
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);
vix::websocket::Server ws{app.config(), executor};
ws.on_message(
[](vix::websocket::Session &session, const std::string &message)
{
session.send_text("echo: " + message);
});
vix::websocket::AttachedRuntime runtime{app, ws, executor};
app.run(8080);
return 0;
}When AttachedRuntime is destroyed, it calls finalize_shutdown() defensively.
Destructor behavior
AttachedRuntime finalizes shutdown in its destructor.
~AttachedRuntime
-> finalize_shutdown()This makes cleanup safer if the user forgets to finalize manually.
Still, when building advanced lifecycle code, explicit shutdown is clearer.
Idempotent shutdown
Shutdown APIs are designed to be safe when called more than once.
For example:
runtime.request_stop();
runtime.request_stop();
runtime.finalize_shutdown();
runtime.finalize_shutdown();Only the first meaningful call performs the shutdown work.
This helps when shutdown can be triggered by several paths:
- Ctrl+C
- HTTP shutdown route
- process signal
- destructor cleanup
- internal error
- test teardown
Session shutdown flow
A normal session lifecycle ends like this:
close requested
-> close frame if possible
-> stop heartbeat
-> close stream
-> notify router close once
-> cleanupDuring forced server shutdown:
server stop_async
-> session.shutdown_now()
-> transport closes immediately
-> pending operations are cancelled or failClose notification
A session should notify close handlers only once.
This avoids duplicate cleanup logic.
Conceptually:
close event
-> compare close-notified flag
-> notify router close onceThis is important because close can happen from several paths:
- client close frame
- read error
- write error
- server shutdown
- forced shutdown
- protocol error
Shutdown with rooms
When a session closes, the server removes it from all rooms.
Conceptually:
session close
-> unregister session
-> remove session from all rooms
-> call user close handlerThis prevents room lists from keeping dead sessions.
Shutdown with long-polling
Long-polling uses HTTP polling sessions and in-memory buffers.
During application shutdown:
stop WebSocket server
-> stop accepting realtime connections
-> stop executor
-> let process cleanup long-polling managerIf your application owns a LongPollingBridge or LongPollingManager, keep its lifetime longer than the server callbacks that use it.
Shutdown with metrics exporter
run_metrics_http_exporter(...) is a blocking helper.
If you run it in a separate thread, coordinate shutdown for that thread in your application.
Example model:
main app
-> starts metrics exporter thread
-> starts HTTP + WebSocket
-> shutdown requested
-> stop WebSocket
-> stop HTTP
-> stop metrics exporter path
-> join metrics threadThe current minimal exporter is intended as a simple metrics endpoint.
Advanced applications may expose metrics from the main HTTP app instead.
Shutdown with message store
Message stores should outlive callbacks that use them.
Example:
vix::websocket::SqliteMessageStore store{"messages.db"};
ws.on_typed_message(
[&store](vix::websocket::Session &session,
const std::string &type,
const vix::json::kvs &payload)
{
(void)session;
(void)type;
(void)payload;
// store is still alive while callback can run
});During shutdown, make sure no callback can use the store after it has been destroyed.
The easiest rule is:
create store before server starts
destroy store after server stopsRecommended object lifetime
A safe object lifetime order is:
create config
create executor
create message store
create metrics
create long-polling bridge
create WebSocket server
register callbacks
start server
stop server
stop executor
destroy bridge
destroy metrics
destroy storeFor attached runtime:
create app
create executor
create stores/metrics/bridges
create WebSocket server
register callbacks
create AttachedRuntime
run app
AttachedRuntime finalizes shutdown
destroy stores/metrics/bridgesSignal handling model
A good signal handling model is:
signal received
-> set atomic stop flag
-> request non-blocking stop
-> main control path notices flag
-> finalize blocking shutdownAvoid doing heavy shutdown directly inside a low-level signal handler.
Example: manual shutdown flag
#include <atomic>
#include <chrono>
#include <memory>
#include <thread>
#include <vix/config/Config.hpp>
#include <vix/executor/RuntimeExecutor.hpp>
#include <vix/websocket.hpp>
int main()
{
std::atomic<bool> stopRequested{false};
vix::config::Config config{".env"};
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);
vix::websocket::Server ws{config, executor};
ws.on_message(
[](vix::websocket::Session &session, const std::string &message)
{
session.send_text("echo: " + message);
});
ws.start();
while (!stopRequested.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds{100});
}
ws.stop();
executor->stop();
return 0;
}This example shows the control-path shape.
The code that sets stopRequested depends on your application.
Example: close one client on command
ws.on_message(
[](vix::websocket::Session &session, const std::string &message)
{
if (message == "close")
{
session.close("client requested close");
return;
}
session.send_text("echo: " + message);
});Use this when one client should be disconnected without stopping the server.
Example: stop server from an external control path
void stop_websocket_server(
vix::websocket::Server &ws,
std::shared_ptr<vix::executor::RuntimeExecutor> executor)
{
ws.stop();
if (executor)
{
executor->stop();
}
}Call this from a safe path that can block.
Example: request attached stop
void request_shutdown(vix::websocket::AttachedRuntime &runtime)
{
runtime.request_stop();
}This requests shutdown without blocking.
Example: finalize attached shutdown
void finalize_shutdown(vix::websocket::AttachedRuntime &runtime)
{
runtime.finalize_shutdown();
}Call this from a path where blocking cleanup is safe.
Shutdown and executor
The executor should stop after the WebSocket server is stopped.
Recommended order:
ws.stop()
executor.stop()This lets server cleanup happen before the shared executor is stopped.
AttachedRuntime::finalize_shutdown() follows this order.
Shutdown and callbacks
Callbacks may still run while sessions are closing.
Make sure captured references remain valid.
Avoid capturing references to short-lived objects.
Good:
auto sharedState = std::make_shared<MyState>();
ws.on_message([sharedState](vix::websocket::Session &session,
const std::string &message)
{
(void)session;
(void)message;
sharedState->count++;
});Risky:
MyState state;
ws.on_message([&state](vix::websocket::Session &session,
const std::string &message)
{
(void)session;
(void)message;
state.count++;
});Reference captures are fine only when the referenced object clearly outlives the server.
Shutdown and blocking work
Avoid blocking inside:
on_openon_messageon_closeon_error- HTTP shutdown callbacks
- low-level server callbacks
If heavy work is needed, schedule it through a safe application service or runtime path.
Common shutdown mistakes
Calling stop from inside a shutdown callback
Avoid blocking final shutdown inside a callback.
Prefer:
ws.stop_async();or:
runtime.request_stop();Then finalize later.
Destroying dependencies too early
Avoid destroying a store, bridge, or metrics object while callbacks may still use it.
Stop the server first.
Stopping executor too early
Avoid:
executor->stop();
ws.stop();Prefer:
ws.stop();
executor->stop();Using shutdown_now for normal app logic
Avoid:
session.shutdown_now();for normal user disconnects.
Prefer:
session.close("reason");Forgetting to join threads
For standalone usage, make sure final shutdown happens.
ws.stop();stop_async() alone only requests shutdown.
Best practices
Use session.close(...) for one-client application closes.
Use ws.stop_async() for non-blocking server stop requests.
Use ws.stop() for final standalone cleanup.
Use runtime.request_stop() in attached non-blocking paths.
Use runtime.finalize_shutdown() for attached final cleanup.
Stop WebSocket before stopping the executor.
Keep stores, bridges, and metrics alive until after server shutdown.
Avoid blocking inside callbacks.
Make shutdown idempotent.
Next steps
Continue with: