On This Page

Supervise a Critical Bot Worker

Keep gateway and worker health separate, then choose a fail or restart policy

A bot started with runBot is already supervised: The runner watches the connection and every subscription it registered, and stops the bot with CriticalWorkerStoppedError if one closes. That includes a handler or command router set to overflow: "stop" whose queue fills, and the error then names the event and the exceeded limit. This page is for applications that create the client with createClient and need the same guarantee together with a health report

A connected bot can still have a stopped event subscription, the registration that delivers one event to its handler. A failed handler does not stop later events, and a full queue drops the oldest waiting event by default. A subscription registered with overflow: "stop" closes when its queue fills instead. A subscription the bot cannot work without is called a critical worker here. Check both the connection and each critical worker

Watch the Connection and Subscription

This example answers !ping and stops the bot if its messageCreate subscription stops. It sets overflow: "stop", so a burst the bot cannot keep up with counts as a failure instead of silently dropping messages. Health reports contain error types and counts, never event bodies or raw exceptions

import { createClient, } from "@neontechspace/fluxerly"

export async function runSupervisedBot(token, signal, reportHealth) {
    const client = createClient({ token })
    let criticalWorker = "starting"
    let handlerFailures = 0
    const lifetimes = []
    let outcome
    let primaryFailure
    let hasPrimaryFailure = false
    const localStop = new AbortController()

    const report = () => reportHealth({ gateway: client.state, criticalWorker, handlerFailures }, client.diagnostics())
    const gatewayObserver = client.observeState(report)

    try {
import {
    createClient,
    type ClientDiagnostics,
    type ConnectionState,
} from "@neontechspace/fluxerly";

export interface BotHealth {
    readonly gateway: ConnectionState;
    readonly criticalWorker: "starting" | "running" | "failed" | "stopped";
    readonly handlerFailures: number;
}

export type SupervisedOutcome =
    | { readonly kind: "stopped" }
    | { readonly kind: "connection-failed"; readonly failure: string }
    | { readonly kind: "critical-worker-failed"; readonly failure: string };

The health callback can feed a readiness endpoint or a process manager. The diagnostics snapshot describes gateway state, request pressure and local queues. It does not establish that a critical worker is alive, so the separate worker state remains necessary

The subscription's onError counts an isolated handler failure while the subscription continues. Because of overflow: "stop", a full queue closes the subscription, changes the worker state to failed and makes waitForClose return EventOverflowError. The example then aborts client.run() and waits for the connection, worker and final cleanup

Choose the Application Policy

If a process manager runs the bot, exit with a failure status after critical-worker-failed so it can start a new process. To restart within the same process, call runSupervisedBot again with a new client and a limit on restart attempts

A bot split across processes by the supervisor gets the process-manager policy by default: When a child process exits unexpectedly, the parent starts a new one for the same shards, up to three times in a row. The parent's workers.status() also includes each child's latest diagnostics snapshot, so one place shows every child's gateway state and request pressure

A restart subscribes only to future events. It does not rerun the failed handler or recover events missed while the subscription was stopped. For work that must survive a restart, the application needs a durable queue, rules to prevent duplicate work and a way to check what already completed

Noncritical subscriptions can keep their existing isolated-failure policy. Their handler failure does not close this critical subscription or the client. If a noncritical worker must affect readiness, supervise its waitForClose() explicitly too

Limit Subscriptions Created While the Bot Runs

Each SDK subscription and collector limits its own queue, but nothing caps how many subscriptions the client can have in total. Register workers at startup where possible

Budget Workers Created at Runtime

Keep a reviewed list of workers registered at startup. If the application creates workers dynamically, route every such creation through one application-owned budget. Reserve capacity before registration and release it only after registration fails or the worker's waitForClose() and required cleanup finish. A wrapper cannot enforce the budget if other code can register directly, so restrict direct client access to code that applies the same budget

The diagnostics.events snapshot reports this client's open event sources, message collectors, reaction collectors and currently executing subscription callbacks. Use those payload-free counters to monitor usage and alert near the budget's limits. They are not enforced caps, process-memory measurements or counts of collector filters and arbitrary application tasks. The application's reservations determine whether another worker can register

Drain Application-Owned Work

Default handlers receive an AbortSignal, but the application must still track its own Promises, even when they respond to cancellation. Returning a Promise lets the SDK wait for that handler's work and report its failure. Closing a subscription does not wait for it, and neither does a plain client.shutdown(), which cancels running handlers at once

A shutdown can drain first. With client.shutdown({ drainMs: 10_000 }), the client stops accepting new events, lets running handlers, their waiting events, running scheduled tasks and REST requests finish for up to that many milliseconds, then cancels what is left. The runBot runner drains this way when asked to stop, for up to its drainMs option, 5,000 ms by default. In both APIs, a child of the supervisor drains when the parent stops it, including an automatic move to a larger shard plan, for up to the supervisor's shutdownTimeoutMs minus one second, which is 4 seconds with the default of 5,000 ms. A child failure or loss of the parent's message channel stops running work without draining. The drain covers only work that handlers return and scheduled tasks, not detached Promises

Collector progress callbacks behave differently: Their completion waits for the returned callback work. Native Effect handlers use interruption and awaited scoped finalizers, as shown in the Effect application testing guide

When an external operation must finish before process exit, retain its Promise in an application-owned set. Remove it on either settlement path without creating an unobserved rejected Promise. Stop intake first, then inspect the outcomes of the retained work

export function installTrackedHandler(client, handle) {
    const active = new Set()
    const registered = client.on("messageCreate", (message, signal) => {
        const work = Promise.resolve().then(() => handle(message, signal))
        active.add(work)
        void work.then(() => active.delete(work), () => active.delete(work))
        return work
    })

    return {
        subscription: registered,
        async stopAndDrain() {
            registered.close()
            const failures = []
            try {
                const closed = await registered.waitForClose()
import type { Client, Message, OperationSignal } from "@neontechspace/fluxerly";

export function installTrackedHandler(
    client: Client,
    handle: (message: Message, signal: OperationSignal) => Promise<void>,
) {
    const active = new Set<Promise<void>>();
    const registered = client.on("messageCreate", (message, signal) => {
        const work = Promise.resolve().then(() => handle(message, signal));
        active.add(work);
        void work.then(() => active.delete(work), () => active.delete(work));
        return work;
    });

    return {
        subscription: registered,

The application calls stopAndDrain() outside the tracked handler to avoid joining itself. The returned function drains work still active after intake stops, not a history of earlier failures. The ordinary handler-error hook remains responsible for earlier failures

An operation that ignores cancellation can keep this drain pending. A chosen application deadline bounds waiting only if the underlying service also has a supported cancellation or recovery policy. A timeout alone cannot prove that an external write stopped or was rolled back