Skip to content
GuidesDocs 1.3.x

Server Lifecycle ​

Connectum servers follow a deterministic state machine. Understanding the lifecycle helps you hook into the right moment for health checks, telemetry, background workers, and resource cleanup.

Server States ​

StateWhat happens
createdcreateServer() returns. No port is bound yet.
startingserver.start() called -- the server binds the port and initializes protocols.
runningThe server is accepting requests.
stoppingserver.stop() called (or a signal received with autoShutdown). Connections are being drained.
stoppedStartup failed or shutdown finished. With forceCloseOnTimeout: false, existing connections may remain open; a failed hook may leave application cleanup incomplete.

The transitions are one-directional. A stopped server cannot be restarted -- create a new one instead.

addService(), addInterceptor() and addProtocol() work only in the created state. Each throws once the server is in any other state (starting, running, stopping or stopped), and also once the routes have been built: start() builds them, and so does the first use of the in-process client (server.localClient()), even on a server that is still in the created state.

Lifecycle Events ​

Register listeners with server.on(event, handler):

typescript
server.on('start', () => {
  console.log('Server is starting...');
});

server.on('ready', () => {
  console.log(`Listening on port ${server.address?.port}`);
  healthcheckManager.update(ServingStatus.SERVING);
});

server.on('stopping', () => {
  console.log('Shutdown initiated');
  healthcheckManager.update(ServingStatus.NOT_SERVING);
});

server.on('stop', () => {
  console.log('Server stopped cleanly');
});

server.on('error', (err) => {
  console.error('Server error:', err);
});

Event Reference ​

EventPayloadTypical use
start--Log startup, initialize external connections
ready--Update health status to SERVING, start background workers
stopping--Update health status to NOT_SERVING, stop accepting new work
stop--Final log, exit the process if needed
errorErrorLog the error, alert monitoring

Event Ordering ​

Events always fire in the transition order shown in the state diagram: start, ready, stopping, then stop.

error may fire at any point. If startup fails before the server is listening, or a start listener throws, the sequence is start → error. If a ready listener throws, ready has already been emitted, so the sequence is start → ready → error. If the shutdown itself fails (closing the transport or a shutdown hook), it is stopping → error → stop: a failed shutdown still ends with stop, and stop() rejects with the original error, or with an AggregateError when the transport close and a hook both failed (see Graceful Shutdown). A stopping or stop listener that throws is not a failed shutdown: it is reported through error and stop() still resolves.

Listener Exceptions ​

What happens when a listener throws depends on the phase:

EventA throwing listener
start, readyIs a startup failure. start() rejects with that exception, error is emitted, the port, the autoShutdown signal handlers and the event bus are released, and the server ends up stopped.
stopping, stopIs isolated. The remaining listeners of the event still run, the shutdown always reaches stopped, and the exception is reported through error. stop() does not reject because of it.

If no error listener is registered, an exception from a stopping or stop listener is printed with console.error instead of being swallowed. A failed shutdown started by a signal (autoShutdown) is reported once through error and never becomes an unhandled rejection.

Only synchronous exceptions are covered. An async listener that rejects is not awaited by the emitter; handle its errors inside the listener.

The shutdownSignal ​

Every server exposes an AbortSignal via server.shutdownSignal. The signal is aborted when the server enters the stopping state. Use it to propagate cancellation to streaming RPCs, background workers, and long-running operations.

typescript
server.on('ready', () => {
  startBackgroundWorker(server.shutdownSignal);
});

function startBackgroundWorker(signal: AbortSignal) {
  const interval = setInterval(() => {
    if (signal.aborted) {
      clearInterval(interval);
      return;
    }
    // Periodic work
  }, 5_000);
}

With Fetch or Timers ​

Node.js built-in APIs accept AbortSignal natively:

typescript
import { setTimeout } from 'node:timers/promises';

// Cancels if the server shuts down before the delay completes
await setTimeout(10_000, undefined, { signal: server.shutdownSignal });
typescript
// Abort an outgoing HTTP request when the server shuts down
const res = await fetch('https://api.example.com/data', {
  signal: server.shutdownSignal,
});

Integrating with Health Checks ​

The @connectum/healthcheck package exposes a healthcheckManager singleton. Update it in lifecycle events so that load balancers and Kubernetes know when the service is ready:

typescript
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';

const server = createServer({
  services: [routes],
  protocols: [Healthcheck({ httpEnabled: true })],
  shutdown: { autoShutdown: true },
});

server.on('ready', () => {
  healthcheckManager.update(ServingStatus.SERVING);
});

server.on('stopping', () => {
  healthcheckManager.update(ServingStatus.NOT_SERVING);
});

await server.start();

The stopping handler sets the reported status to NOT_SERVING. Probes observe the change on their next request; endpoint withdrawal also depends on the orchestrator. For the shutdown order and available drain time, see Graceful Shutdown.

Complete Example ​

Use the service definition from Quickstart and the startBackgroundWorker() implementation above.

typescript
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
import { createDefaultInterceptors } from '@connectum/interceptors';
import { greeterService } from './services/greeterService.ts';

const server = createServer({
  services: [greeterService],
  port: 5000,
  protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
  interceptors: createDefaultInterceptors(),
  shutdown: { autoShutdown: true, timeout: 25_000 },
});

server.on('start', () => console.log('Starting...'));
server.on('ready', () => {
  console.log(`Ready on port ${server.address?.port}`);
  healthcheckManager.update(ServingStatus.SERVING);
  startBackgroundWorker(server.shutdownSignal);
});
server.on('stopping', () => {
  healthcheckManager.update(ServingStatus.NOT_SERVING);
});
server.on('stop', () => console.log('Stopped'));
server.on('error', (err) => console.error('Error:', err));

await server.start();