Skip to content

Kernel & Boot

The kernel is the one object each machine boots — server and client each run exactly one. It owns the Scheduler, the hook registry, the Bus, and the service manager, and it contains no game rules. The defining design decision is the two-layer split: the shared microkernel in ReplicatedStorage.ChloeKernel replicates to every client and holds only generic primitives, while ServerScriptService.ChloeKernelServer never replicates and holds everything privileged — sessions, networking, validators, rate limits. Code placement is the attack surface; see Architecture & security.

Kernel.boot() is construction, kernel:start() is ignition, and the gap between them is the registration window.

  • boot builds the kernel: a Heartbeat-bound scheduler, a hook registry, a bus, an empty service manager. It also connects Scheduler.OnError to a warning sink, so a crashing task never takes the frame down silently. The result is stored in a module-level singleton — a second boot() on the same machine errors with ChloeKernel is already booted on this machine; use Kernel.current().
  • the registration window is where your game exists. Main.server.luau and Main.client.luau both look for a sibling Bootstrap ModuleScript and call it with the booted kernel. Everything you register — services, drivers, channels — happens here, before any of it runs.
  • start runs the two-phase service boot (every init before any start, dependency-ordered — see Services), prints one line ([ChloeKernel 0.5.0] Server booted N services in Xms), and stamps the kernel version on a ReplicatedStorage attribute (ChloeKernelServer or ChloeKernelClient) so external tooling can observe kernel state without sharing its VM.

The version is Kernel.Version = "0.5.0". kernel.Role is "Server" or "Client", decided by RunService:IsServer().

Both Main scripts ship with the framework and already do this — you edit Bootstrap, not Main.

Server (src/Server/Main.server.luau):

  1. In Studio only: TestKit runs every spec under the shared and server Tests/ folders. A failing spec fails the play test before the kernel exists.
  2. ServerKernel.boot() — boots the shared core underneath, wraps it, and wires session creation to Players.PlayerAdded/PlayerRemoving (including players already present).
  3. The sibling Bootstrap ModuleScript is required and called with the booted kernel.
  4. kernel:start() — two-phase service boot. If a NetDriver was built during bootstrap, every intent or request channel that has a handler but no validator and is not Open gets a boot warning: it will reject every payload.
  5. In Studio only: enableDiagnostics(1) feeds the F8 debug panel, a deferred securityAudit asserts on any High-severity finding, and the fuzzer drives hostile payloads through every channel once the first session exists. Both gates fail the play test loudly.

Client (src/Client/Main.client.luau):

  1. Kernel.boot() — the client runs the shared microkernel directly; there is no privileged layer to add.
  2. The sibling Bootstrap is required and called.
  3. kernel:start().
  4. Panel.attach(kernel) — the debug panel, Studio-only unless the ChloeKernelDebug attribute is set.

boot(config?) takes a KernelConfig with two optional numbers. They configure how much of each frame the scheduler may spend; the full mechanics live on the Scheduler page.

Option Type Default Effect
BudgetSeconds number? 0.0015 Without TargetFrameSeconds this is a fixed per-step slice. With it, it is the floor the frame-aware budget never drops below.
TargetFrameSeconds number? 0.0165 on the server when neither option is set; otherwise nil Enables frame-aware budgeting: each step spends the slack left under this target after the engine’s measured share of the frame.

The rule, exactly as the source decides it: frame-aware mode turns on when TargetFrameSeconds is set explicitly, or when neither option is set on the server. Passing only BudgetSeconds gives a fixed slice on either machine.

  • Server default — frame-aware. Each step reads Stats.HeartbeatTimeMs and Stats.PhysicsStepTimeMs, subtracts the engine’s share (minus the scheduler’s own smoothed step cost, so idle frames the scheduler fills do not read as busy), and spends the slack under the 16.5ms target — never less than the 1.5ms floor. An idle server drains backlog fast; a physics-heavy one backs off automatically. Late frames back off to the floor regardless of engine stats, which catches overload and costs the stats miss (replication serialization, for one).
  • Client default — fixed 1.5ms. HeartbeatTimeMs excludes render time on the client, so a frame-aware budget would read an empty frame and starve the render thread. The client stays on a flat slice unless you explicitly opt in.
-- Main.server.luau owns the real boot call; shown for orientation
local ServerKernel = require(game:GetService("ServerScriptService").ChloeKernelServer)
local Booted = ServerKernel.boot({
TargetFrameSeconds = 0.020, -- frame-aware against a 50fps target
BudgetSeconds = 0.002, -- and never below 2ms even on late frames
})

Registration happens in Bootstrap, which receives the booted kernel between boot() and start():

-- src/Server/Bootstrap.luau
return function(kernel)
kernel:registerService({
Name = "Score",
init = function(self, booted)
self.Kernel = booted
self.Totals = {}
end,
start = function(self)
self.Kernel:onSession(function(session)
self.Totals[session.Player] = 0
session:bind(function()
self.Totals[session.Player] = nil
end)
end)
end,
})
end

Anywhere else, after boot:

local Kernel = require(game:GetService("ServerScriptService").ChloeKernelServer).current()
local Score = Kernel:getService("Score")

Kernel (shared, ReplicatedStorage.ChloeKernel)

Section titled “Kernel (shared, ReplicatedStorage.ChloeKernel)”
Member Description
Kernel.boot(config: KernelConfig?): Kernel Once per VM; errors if already booted. Builds and binds the scheduler, hooks, bus, services.
Kernel.current(): Kernel The booted instance; asserts ChloeKernel has not booted yet before boot.
Kernel.Version "0.5.0".
Kernel.Priority { Kernel = 1, High = 2, Normal = 3, Low = 4, Background = 5 } — lower runs first.
kernel.Role "Server" or "Client".
kernel.Scheduler, .Hooks, .Bus, .Services The core subsystems, directly reachable.
kernel:registerService(definition) Forwards to Services:register; see Services.
kernel:start() Two-phase service boot; errors kernel already started on a second call.
kernel:getService(name: string) The service table; errors on unknown names.
kernel:spawnProcess(fn, options?): Process options = { Name: string?, Priority: number? }; binds the process to the kernel scheduler. See Processes.
kernel:phaseScheduler(phase): Scheduler Lazily builds a scheduler bound to another engine phase.
kernel:stats() { Version, Role, Scheduler = { LastStepSeconds, TasksRun, TasksDeferred }, ServiceTimings }.
kernel:enableDiagnostics(intervalSeconds?, extend?) Publishes health attributes every interval (default 5s); idempotent.
kernel:shutdown() Full teardown; clears the boot singleton so a fresh boot() works.
kernel:api(extensions?) Frozen restricted surface for userspace code.

ServerKernel (ServerScriptService.ChloeKernelServer)

Section titled “ServerKernel (ServerScriptService.ChloeKernelServer)”

Everything above, wrapped, plus:

Member Description
ServerKernel.boot(config?): ServerKernel Boots the shared core underneath and wires sessions. Same config, own singleton.
ServerKernel.current(): ServerKernel Asserts ChloeKernelServer has not booted yet before boot.
kernel.Core The underlying shared kernel.
kernel.Sessions Live map of Player to Session.
kernel.OnSessionStart Signal fired with each new session.
kernel:onSession(fn): Connection Fires for current and future sessions; see Sessions.
kernel:getSession(player): Session? nil after the player leaves.
kernel:net(): NetDriver Lazy singleton — games that never touch networking never build the driver.
kernel:stats() Core snapshot plus Sessions (count) and Net (a clone of the driver’s counters, when built).
kernel:enableDiagnostics(intervalSeconds?) Core diagnostics extended with session and net counters, plus a debug-gated log mirror.
kernel:securityAudit(options?): { AuditFinding } options = { Silent: boolean? }. One pre-ship sweep; findings sorted most severe first.
kernel:api() The core surface extended with onSession and getSession.

The server kernel also points Services.Kernel at itself, so a service’s init(self, kernel) receives the server kernel — session API included — not the bare core.

The main scheduler steps on Heartbeat. kernel:phaseScheduler(phase) lazily builds and caches one scheduler per additional engine phase:

Phase Use Notes
"Heartbeat" Default Returns the main scheduler itself.
"PreSimulation" Physics-coupled work before the solver Fixed-budget.
"PostSimulation" Work that reads solved physics Fixed-budget.
"PreRender" Camera work Client-only — binding on the server errors PreRender schedulers are client-only.

Phase schedulers are deliberately fixed-budget (they inherit the main scheduler’s BudgetSeconds, not its frame-target): a frame-aware phase scheduler would measure the same frame slack Heartbeat already spent and double-claim it. Each gets its own OnError warning sink naming the phase.

local Physics = Kernel:phaseScheduler("PreSimulation")
Physics:every(0, function()
-- runs every physics step, before the solver, under a 1.5ms slice
applyCustomForces()
end, Kernel.Priority.High)

kernel:stats() is a cheap point snapshot — scheduler health plus per-service boot timings. kernel:enableDiagnostics(intervalSeconds?) is the continuous version: every interval (default 5s) it publishes a health snapshot to ReplicatedStorage attributes, where the debug panel and external tooling read them without sharing the kernel’s VM. The diagnostics task runs at Kernel priority — health reporting must survive overload, when Background work has already stopped running.

Calling it also attaches GcWatch: Roblox exposes no GC pause timings, so allocation rate, collection cadence, and reclaim size are the actionable signals instead.

Attributes are named ChloeKernelDiag + role + suffix (so ChloeKernelDiagServerStepMs on the server). Step and deferral figures are windowed over the interval — a point sample phase-locks against recurring work and reads whatever frame it aligns with, and deferral is bursty enough that a point sample between bursts reads zero during genuine overload.

Suffix Meaning
BudgetMs Last step’s budget (varies per frame in frame-aware mode).
StepMs / StepMaxMs Windowed average / max scheduler step time.
FrameBusyMs / PhysicsMs Engine heartbeat compute and physics time, broken out so frame time is attributable per source.
FrameTargetMs The frame-aware target (0 when fixed).
TasksRun Cumulative tasks executed.
Deferred Tasks deferred, summed over the window.
Services Booted service count.
LuaHeapMb gcinfo() for this VM (shared with the client heap in Studio, separate in production).
MemoryMb / Instances / Primitives / MovingPrimitives / Contacts Engine memory and simulation counters (MemoryMb only where memory tracking is available).
ReplKbpsIn / ReplKbpsOut / PhysKbpsIn / PhysKbpsOut Replication and physics bandwidth — the only ungated engine signals on live builds.
GcAllocKbS / GcCyclesMin / GcReclaimKb GcWatch: allocation rate, GC cadence, last reclaim size.
HotTasks Top 3 profiler entries as ms-per-second name, pipe-separated.
Uptime Seconds since diagnostics started.

On the server the snapshot is extended with Sessions and Net, and the net counters publish under role-less names: ChloeKernelDiagNetAccepted, NetRejected, NetRateLimited, NetTransportFloods, NetTransportFloodBytes.

Overload alerting: after three consecutive diagnostic windows with deferred tasks, the kernel publishes Kernel.Overload on the Bus with the stats snapshot:

Kernel.Bus:subscribe("Kernel.Overload", function(_, snapshot)
Log.warn("sustained frame budget overruns", snapshot.Scheduler.TasksDeferred)
end)

The shared enableDiagnostics accepts an extend callback that mutates the snapshot before publishing — that is how the server layer grafts Sessions and Net on. A crashing extension warns instead of failing silently. The server variant additionally wires a log mirror when running in Studio or when ChloeKernelDebug is set: Logger lines buffer in a 32-line ring and flush every 0.5s over a CKDBG_LogTail packet — sent per recipient, never broadcast, and only to allowlisted debuggers, because logs can carry anything.

kernel:api(extensions?) returns a frozen table for userspace code — scripts that should schedule work and talk on the bus without reaching scheduler internals or firing kernel-reserved hooks.

Member Forwards to
Version, Role, Priority Constants.
schedule(fn, priority?, ...) Scheduler:schedule — one-shot, next step.
every(intervalSeconds, fn, priority?) Scheduler:every.
spawnProcess(fn, options) kernel:spawnProcess.
publish(topic, ...) / subscribe(topic, fn) The Bus.
onHook(name, fn, priority?) Hooks:on, with a guard: any name starting with Kernel. errors — those hook points are kernel-reserved.
getService(name) Services:get.

Privileged layers pass extensions to graft their own members on before the freeze — the server kernel adds onSession and getSession this way. Because the table is frozen, userspace cannot swap members out from under other consumers.

local Api = ServerKernel.current():api()
Api.every(1, function()
Api.publish("Game.Tick")
end, Api.Priority.Background)

kernel:securityAudit({ Silent? }?) sweeps the security surface in one call and returns { Severity, Kind, Name, Detail } findings, sorted most severe first. Unless Silent, it prints security audit: clean or a warning block listing every finding.

Kind Severity Meaning
UnguardedChannel High A channel has a handler but no validator and is not Open — every payload rejects.
FailOpenGate High An Intent. or Request. hook point is FailOpen — a crashing validator would pass the payload.
OpenChannel Medium Open = true: any client payload reaches the handler unvalidated.
DefaultRateLimit Info The channel rides the default rate limit instead of an explicit one.

Without a NetDriver the sweep still checks hook gates for the fail-open misconfiguration. The Studio boot gate asserts no High finding survives a play test. Full treatment — including the fuzzer that exercises what the audit only reads — is on Fuzz & Audit.

Name Type Fired
Kernel.Overload Bus topic Three consecutive diagnostic windows saw deferred tasks; carries the stats snapshot.
Kernel.SessionStart / Kernel.SessionEnd Bus topics Player join/leave, published by the server kernel; carries the session. See Sessions.
Session.Start / Session.End Hook points Fired synchronously before the matching bus topic. See Sessions.

The full cross-module list lives at Hooks & bus topics.