How RuntimeManager in celld Handles Multiple Durable Object Classes and Service Workers

The RuntimeManager orchestrates stateless workers, co-hosted workers, and service-binding workers through dedicated registries that map Durable Object class names to worker configurations and service names to runtime pools, ensuring unambiguous request routing across the denoland/celld runtime.

The RuntimeManager in the denoland/celld repository serves as the central orchestrator for complex Worker deployments. It enables a single celld instance to host multiple Durable Object (DO) classes alongside stateless service workers while maintaining strict isolation and deterministic routing through specialized data structures defined in crates/celld/runtime.rs.

Core Data Structures for Multi-Class Support

The manager maintains four primary data structures (lines 51-59 in crates/celld/runtime.rs) to track different worker types:

  • stateless: StatelessRuntime — The pool that runs the primary (stateless) Worker script.
  • services: Arc<HashMap<String, StatelessRuntime>> — Maps each service-binding script name to its own runtime pool, enabling fetch_service and rpc_service calls.
  • cell_configs: Arc<HashMap<String, Arc<WorkerConfig>>> — Maps each DO class name to the WorkerConfig (defined in crates/celld/worker.rs) that implements it, guaranteeing a unique configuration per class.
  • cells: Arc<Mutex<CellRegistry>> — Tracks the lifecycle of individual cell isolates (started, published, stopped).

These structures allow the runtime to distinguish between a service call (stateless) and a Durable Object invocation (stateful) without ambiguity.

Registering Multiple Durable Object Classes

When RuntimeManager::start initializes the system, it validates and registers DO classes from both the primary worker and co-hosted scripts to prevent routing conflicts.

Primary Worker Registration

The manager first creates the primary StatelessRuntime for the main script, then collects all DO class names from worker.do_classes into primary_classes. It registers each class in cell_configs while checking for duplicates (lines 74-78):

for class in primary_classes {
    if cell_configs.insert(class.clone(), config.clone()).is_some() {
        return Err(anyhow!("duplicate Durable Object class {class}"));
    }
}

Co-hosted Worker Validation

For each co-hosted worker in the cohosted vector, the manager spins up a dedicated StatelessRuntime pool, inserts it into the services map using the script name as the key, and registers all exported DO classes (lines 79-102):

for target in cohosted {
    // … create `pool` …
    service_pools.insert(script.clone(), pool)?;
    for class in target_classes {
        if cell_configs.insert(class.clone(), config.clone()).is_some() {
            return Err(anyhow!(
                "Durable Object class {class} is exported by more than one co-hosted script"
            ));
        }
    }
}

This validation ensures that duplicate class names across scripts trigger a clear error, preventing ambiguous routing during cell instantiation.

Dispatching Requests to Service Workers

Service-binding calls route through the services HashMap, which maintains separate runtime pools for each named service script.

fetch_service Implementation

The fetch_service method (lines 98-124) looks up the script name in the services map, creates a request ID, and forwards the request to the appropriate StatelessRuntime:

let resp = runtime
    .fetch_service("kv_service", "https://kv.example/key".into(),
                   "GET".into(), vec![], vec![], None)
    .await?;

If the script name is not registered, the method returns a detailed error, guaranteeing deterministic behavior.

rpc_service Implementation

Similarly, rpc_service (lines 126-138) performs the same lookup and invokes StatelessRuntime::rpc on the target pool. Both methods maintain isolation between different service workers while allowing the primary runtime to coordinate them.

Cell Lifecycle Integration

When a request targets a specific Durable Object instance via fetch_cell or rpc, the manager consults cell_configs to resolve the class name to its WorkerConfig. This configuration determines which script spawns the isolate. The publish_cell and start_cell logic rely on this mapping to ensure the correct script runs the right class, with storage operations handled separately in crates/celld/storage.rs.

Complete Configuration Example

To initialize a RuntimeManager with multiple DO classes and service workers:

use celld::runtime::{RuntimeManager, RuntimeOptions, CohostedWorker, AlarmObserver};

let runtime = RuntimeManager::start(RuntimeOptions {
    // Primary script exposing DO classes A and B
    worker: WorkerConfigOptions {
        script_name: "main.js".into(),
        do_classes: vec!["A".into(), "B".into()],
        ..Default::default()
    },
    // Service-binding worker
    services: vec![("kv_service".into(), "kv.js".into(), None)],
    // Co-hosted scripts with unique DO classes
    cohosted: vec![
        CohostedWorker {
            options: WorkerConfigOptions {
                script_name: "chat.js".into(),
                do_classes: vec!["ChatRoom".into()],
                ..Default::default()
            },
            services: vec![],
            asset_binding: None,
            workers: 2,
        },
        CohostedWorker {
            options: WorkerConfigOptions {
                script_name: "analytics.js".into(),
                do_classes: vec!["Analytics".into()],
                ..Default::default()
            },
            services: vec![],
            asset_binding: None,
            workers: 2,
        },
    ],
    workers: 4,
    data_dir: std::path::PathBuf::from("/var/celld/data"),
    replication: None,
    wake: None,
    alarm_observer: Arc::new(|_, _| ()),
    node: "node-1".into(),
    region: "us-east".into(),
    asset_binding: None,
    loader_binding: None,
})?;

Fetching a Durable Object instance uses the class name prefix to locate the correct configuration:

let cell = "ChatRoom:room42".to_string();
let resp = runtime
    .fetch_cell(cell.clone(), None, RuntimeFetch {
        url: "/".into(),
        method: "GET".into(),
        body: vec![],
        headers: vec![],
        request_id: None,
    }, None)
    .await?;

Summary

  • Global class-to-config mapping via cell_configs enables the RuntimeManager to support multiple Durable Object classes with unique worker configurations.
  • Script-to-runtime mapping via services isolates service-binding workers while allowing coordinated dispatch.
  • Strict validation during startup prevents duplicate class names across primary and co-hosted scripts.
  • Unified dispatch layer handles both stateless service calls and stateful Durable Object requests through separate but coordinated code paths.

Frequently Asked Questions

How does RuntimeManager prevent duplicate Durable Object class names?

During initialization in crates/celld/runtime.rs (lines 74-102), the manager inserts each class name into cell_configs and checks the return value. If insert returns Some, indicating the key already exists, it returns an error stating the class is exported by more than one script. This ensures each DO class maps to exactly one worker configuration.

What is the difference between service workers and co-hosted workers in celld?

Service workers (the services field) are stateless Workers accessed via env.NAME.fetch() or rpc() bindings, stored in a HashMap<String, StatelessRuntime>. Co-hosted workers are full Worker scripts that export their own Durable Object classes, each running in isolated runtime pools but registered in the same cell_configs registry. Service workers handle stateless requests, while co-hosted workers contain stateful DO logic.

How does fetch_service route requests to the correct worker?

The fetch_service method looks up the provided script name in the services HashMap (lines 98-124). If found, it forwards the request to that specific StatelessRuntime pool. If not found, it returns a detailed error. This direct mapping eliminates routing ambiguity by ensuring each service name points to exactly one runtime instance.

Can a single celld instance handle both stateless workers and Durable Objects?

Yes. The RuntimeManager simultaneously maintains a primary stateless runtime for the main Worker script, separate services pools for service-binding scripts, and cell_configs for all Durable Object classes. This architecture allows a single celld node to serve stateless HTTP requests, RPC calls to service workers, and stateful requests to multiple DO classes without conflict.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →