Docs / Architecture / Runtime
v1.0.0

Runtime

The part that actually runs your modules: a small state machine, an ordered start, and a shutdown that waits for work to finish.

STATE MACHINE GRACEFUL STOP SIGNALS

Overview

The runtime is the object that owns your application while it is alive. It loads modules, runs their hooks in order, listens for termination signals, and unwinds everything when it is time to stop.

You rarely build one by hand. createApplication from @zudojs/core creates a runtime for you and hands you an Application wrapped around it.

TWO RUNTIMES, TWO PACKAGES

Zudo ships two runtimes and they are not the same object. @zudojs/core has the one createApplication uses. @zudojs/runtime is a separate, standalone package with its own state names and extra readiness and health features. Everything up to the "Standalone Runtime" section below describes the @zudojs/core one.

For everything in the next four sections:

$ npm install @zudojs/core

These docs follow the framework source. If an export shown here is missing from the version you installed, update to the latest @zudojs release.

Runtime States

A state machine is a rule about which step may follow which. The runtime has six states, and moving to a state that is not allowed from the current one throws InvalidRuntimeTransitionError instead of quietly doing something strange.

  created
     │  start()
     ▼
  bootstrapping  ──── error ────▶  failed
     │                                │
     ▼                                │
  ready                                │
     │  stop()                        │  stop()
     ▼                                ▼
  stopping ─────────────────────▶ stopped
StateWhat it means
createdBuilt, nothing has run. This is the only state you can start from.
bootstrappingLoading modules and running onInitialize then onReady.
readyEvery module is up. The application is serving.
stoppingRunning onShutdown then onDestroy, in reverse order.
stoppedFinished. Terminal — this runtime cannot start again.
failedSomething threw. Terminal, but you can still call stop() to clean up.

The names come from the RuntimeState enum, which is exported so you can compare against it rather than typing strings.

import { createApplication, RuntimeState } from "@zudojs/core"; const app = await createApplication(); await app.start(); console.log(app.applicationRuntime?.state === RuntimeState.READY); // true await app.stop();

APPLICATION STATE IS SEPARATE

app.state is not the same value. It is one of created, initializing, initialized, starting, running, stopping, stopped, failed. The application wraps the runtime, so running on the outside corresponds to ready on the inside.

Startup

Startup is four steps: work out the module order, build each module from its factory, run every onInitialize, then run every onReady. Only after the last onReady does the runtime become ready.

A complete program that starts, reports, and stops:

import { createApplication, defineModule } from "@zudojs/core"; import type { Module } from "@zudojs/core"; const database = defineModule({ id: "database", name: "Database", factory: (): Module => ({ id: "database", name: "Database", onInitialize: () => console.log("database connected"), onDestroy: () => console.log("database closed"), }), }); const api = defineModule({ id: "api", name: "API", dependencies: ["database"], factory: (): Module => ({ id: "api", name: "API", onReady: () => console.log("api listening"), onShutdown: () => console.log("api draining"), }), }); const app = await createApplication({ modules: [api, database], autoStart: true, }); console.log("state:", app.state); await app.stop();

What you should see:

database connected api listening state: running api draining database closed

Notice that api was listed first but database ran first. Declaration order does not matter; the dependencies field does.

Startup behaviour is tunable through runtime.startup:

OptionWhat it doesDefault
autoLoadModulesBuild module instances from their factoriestrue
autoInitializeModulesRun onInitialize during bootstraptrue
autoStartModulesRun onReady during bootstraptrue
continueOnInitializeErrorKeep going when a module fails to initializefalse
continueOnStartErrorKeep going when a module fails in onReadyfalse
timeoutMsGive up on bootstrap after this long; 0 means never0

WATCH OUT

With continueOnInitializeError: true the runtime still reaches ready, but the bootstrap result reports success: false and lists the failures. A half-started application that looks healthy is worse than one that refuses to start, so leave this off unless you check the result.

Graceful Shutdown

Graceful means the application stops accepting new work, finishes what it already accepted, and only then lets go of its resources. The opposite is being killed mid-request.

Shutdown walks the modules in the exact reverse of startup order. Every module gets onShutdown first; once they are all done, every module gets onDestroy.

OptionWhat it doesDefault
autoStopModulesRun onShutdowntrue
autoDestroyModulesRun onDestroytrue
continueOnStopErrorKeep stopping the rest when one module throwstrue
continueOnDestroyErrorKeep destroying the rest when one module throwstrue
timeoutMsAbandon shutdown after this long; 0 means never30000

The two continueOn defaults are true on purpose. During shutdown, one broken module must not strand every other module's cleanup.

Shortening the shutdown budget to five seconds:

const app = await createApplication({ modules: [api, database], runtime: { shutdown: { timeoutMs: 5_000 }, }, });

app.stop() stops the runtime but leaves the application able to start again. app.shutdown() stops it and releases the lifecycle for good — after that, a restart is not possible.

Signal Handling

A signal is how an operating system asks a process to stop. Pressing Ctrl-C sends SIGINT. Docker and Kubernetes send SIGTERM before they eventually force a kill.

The runtime installs handlers when it starts and removes them when it stops, so a signal turns into a graceful shutdown instead of an abrupt exit.

OptionWhat it doesDefault
handleSigintStop gracefully on Ctrl-Ctrue
handleSigtermStop gracefully on SIGTERMtrue
handleSighupStop gracefully on SIGHUPfalse
handleUncaughtExceptionMark failed and stop on an uncaught errortrue
handleUnhandledRejectionMark failed and stop on an unhandled rejectiontrue
forceExitOnSecondSignalExit immediately if a second signal arrives mid-stopfalse
forceExitCodeExit code used by the force exit above1

A container-friendly setup — impatient operators get a hard exit on the second Ctrl-C:

const app = await createApplication({ modules: [api, database], autoStart: true, runtime: { name: "orders-service", mode: "production", signals: { handleSigterm: true, handleSigint: true, forceExitOnSecondSignal: true, forceExitCode: 130, }, shutdown: { timeoutMs: 15_000 }, }, });

DANGER

The runtime never calls process.exit() unless forceExitOnSecondSignal is switched on. That is deliberate: exiting the process is the host application's decision, not the framework's. If your process lingers after a signal, something else is still holding an open handle.

The Standalone Runtime

@zudojs/runtime is a different package with a different runtime. It exists for hosts that assemble the pieces themselves rather than going through createApplication.

Its createRuntime takes explicit dependencies — a map of modules, a logger, a container and an event bus — and returns a Runtime.

import { createRuntime } from "@zudojs/runtime"; const runtime = createRuntime( { modules, logger, container, eventBus }, { applicationName: "worker", environment: "production" }, ); await runtime.start();

Two things it adds over the core runtime:

  • Readiness checks. registerReadinessCheck(name, check) adds a named check; runReadinessChecks() re-runs them all; runtime.readiness reports each result. A newly registered check starts out failing until it is first evaluated, so registering one on a live runtime moves it to degraded until it passes.
  • Health. runtime.health and runtime.status give a structured report suitable for a health endpoint.

DIFFERENT STATE NAMES

This runtime's states are created, initializing, initialized, starting, running, stopping, stopped and failed — plain strings, not the core RuntimeState enum. Do not compare values from one runtime against the other.

The testing helper testRuntime is deliberately not exported from the package root. Import it from @zudojs/runtime/testing.