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

> Discover how celld's RuntimeManager efficiently manages multiple Durable Object classes and service workers using dedicated registries for clear request routing within the denoland/celld runtime.

- Repository: [Deno/celld](https://github.com/denoland/celld)
- Tags: internals
- Published: 2026-08-10

---

**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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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):

```rust
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):

```rust
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`:

```rust
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`](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs).

## Complete Configuration Example

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

```rust
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:

```rust
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`](https://github.com/denoland/celld/blob/main/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.