# How Ladybird Browser Implements Web Worker Support: Process-Per-Worker Architecture

> Discover how Ladybird Browser implements Web Worker support with a process per worker architecture. Learn about JavaScript isolation and IPC communication.

- Repository: [Ladybird/ladybird](https://github.com/LadybirdBrowser/ladybird)
- Tags: internals
- Published: 2026-03-05

---

**Ladybird Browser implements Web Worker support by launching each worker in a dedicated OS process, isolating JavaScript execution from the UI thread while maintaining standard DOM Worker APIs through a robust IPC layer.**

Ladybird Browser provides comprehensive Web Worker support—including Dedicated, Shared, and Service Workers—by adhering to the HTML Living Standard's "run a worker" algorithm while enforcing strict process isolation. This architecture ensures that worker scripts execute in separate OS processes, preventing UI blocking and maintaining security boundaries between workers and the main browser context.

## Process-Per-Worker Architecture in Ladybird

Ladybird’s Web Worker implementation follows a strict **process-per-worker** model. Unlike thread-based implementations that share memory space, Ladybird spawns a separate `WebWorker` executable for each worker instance. This design prevents memory corruption from affecting the main browser process and ensures that long-running scripts cannot freeze the UI.

### Launching the WebWorker Process

When JavaScript code invokes `new Worker('script.js')`, the rendering engine triggers process creation through the helper process system. In [[`Libraries/LibWebView/HelperProcess.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/HelperProcess.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWebView/HelperProcess.cpp#L85), the `launch_web_worker_process` function constructs the command-line arguments and spawns the executable:

```cpp
// Libraries/LibWebView/HelperProcess.cpp (lines 85-105)
ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(Web::Bindings::AgentType type)
{
    Vector<ByteString> arguments;
    arguments.append("--type");
    switch (type) {
    case Web::Bindings::AgentType::DedicatedWorker: arguments.append("dedicated"); break;
    case Web::Bindings::AgentType::SharedWorker:    arguments.append("shared");    break;
    case Web::Bindings::AgentType::ServiceWorker:  arguments.append("service");  break;
    }
    // Socket plumbing establishes IPC connection to browser process
    return launch_server_process<Web::HTML::WebWorkerClient>("WebWorker"sv, move(arguments));
}

```

The function distinguishes between **Dedicated**, **Shared**, and **Service** worker types, passing the appropriate flag to the child process before establishing the IPC socket connection.

### IPC Communication Between UI and Worker Processes

Once spawned, the worker process creates a `ConnectionFromClient` instance to handle IPC commands from the browser. The UI process holds a matching `WebWorkerClient` object that serves as the communication endpoint.

The IPC layer handles three critical operations:

- **`start_worker`** – Receives the script URL, credentials, and environment snapshot to initialize execution
- **`close_worker`** – Signals termination and triggers cleanup
- **Message port transfer** – Serializes `MessagePort` objects for `postMessage` communication

## The WorkerHost Lifecycle Implementation

At the heart of Ladybird’s Web Worker support is the **`WorkerHost`** class, which implements the HTML specification’s *run a worker* algorithm. This class manages the entire lifecycle from realm creation to script execution and termination.

### Creating the JavaScript Realm and Global Scope

When `ConnectionFromClient` receives the start command, it instantiates `WorkerHost` and invokes `WorkerHost::run`. This method creates a fresh JavaScript realm and allocates the appropriate global scope object based on the worker type.

In [[`Services/WebWorker/WorkerHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/WorkerHost.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/WorkerHost.cpp), lines 60-68 handle realm creation:

```cpp
// Services/WebWorker/WorkerHost.cpp (excerpt)
void WorkerHost::run(GC::Ref<Web::Page> page, /* ... */)
{
    // Create a new JavaScript realm with the correct global object
    auto realm_execution_context = Web::Bindings::create_a_new_javascript_realm(
        Web::Bindings::main_thread_vm(),
        [&](JS::Realm& realm) -> JS::Object* {
            if (is_shared)
                return realm.heap().allocate<Web::HTML::SharedWorkerGlobalScope>(realm, page);
            return realm.heap().allocate<Web::HTML::DedicatedWorkerGlobalScope>(realm, page);
        },
        nullptr);
    
    // ... environment setup continues
}

```

The lambda function passed to `create_a_new_javascript_realm` determines whether to instantiate `DedicatedWorkerGlobalScope` or `SharedWorkerGlobalScope`, ensuring that the global `self` object exposes the correct API surface for each worker type.

### Setting Up the EnvironmentSettingsObject

After realm creation, `WorkerHost` constructs a **`WorkerEnvironmentSettingsObject`** to maintain the worker's execution context. This object captures a snapshot of the creating document's origin, Content Security Policy (CSP), and other critical settings required by the Fetch algorithm.

The setup occurs in `WorkerEnvironmentSettingsObject::setup`, which initializes:

- The realm execution context
- The environment settings snapshot (origin, CSP, referrer policy)
- The module map for `importScripts` support
- The global object's bindings

### Fetching the Initial Worker Script

With the environment established, `WorkerHost` initiates the script fetch using the HTML specification's *perform the fetch* hook. In [[`Services/WebWorker/WorkerHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/WorkerHost.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/WorkerHost.cpp), lines 34-98 implement this logic:

```cpp
// Services/WebWorker/WorkerHost.cpp (perform_fetch implementation)
auto perform_fetch = Web::HTML::create_perform_the_fetch_hook(
    inside_settings->heap(),
    [&](auto request, auto top_level, auto process_response) {
        // Use the standard Fetch algorithm
        auto fetch_controller = Web::Fetch::Fetching::fetch(
            inside_settings->realm(),
            request,
            Web::Fetch::Infrastructure::FetchAlgorithms::create(
                inside_settings->heap(),
                {
                    .process_response = move(process_response),
                    .process_response_end_of_body = [&](auto response) {
                        // Update worker URL and environment after fetch completes
                        inside_settings->set_url(response->url().value_or({}));
                    }
                }));
        
        // CSP checks and environment updates occur here
    });

```

This implementation ensures that worker scripts undergo the same security checks (CSP, CORS, referrer policy) as main-thread fetches, while maintaining the worker's isolated environment.

## Worker Global Scopes and APIs

Once the script fetch completes and the realm is initialized, Ladybird exposes the standard Web Worker APIs through specialized global scope classes.

### DedicatedWorkerGlobalScope vs SharedWorkerGlobalScope

Ladybird distinguishes between worker types through separate C++ classes that inherit from `WorkerGlobalScope`:

- **`DedicatedWorkerGlobalScope`** – Created for `new Worker()` instances, exposing `postMessage`, `close`, and `importScripts` methods
- **`SharedWorkerGlobalScope`** – Created for `new SharedWorker()` instances, additionally exposing the `onconnect` event and managing multiple connection ports

Both classes are instantiated in `WorkerHost::run` based on the `is_shared` boolean flag passed during process initialization.

### Message Passing and Port Management

Communication between the main thread and workers relies on the HTML `MessagePort` API. When `WorkerHost::run` executes, it receives a serialized `MessagePort` from the UI process via the `implicit_port` argument.

The implementation in [`WorkerHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/WorkerHost.cpp) (lines 94-99) attaches this port to the global scope:

```cpp
// Message port setup in WorkerHost::run
auto implicit_port = Web::HTML::TransferDataDecoder::decode_message_port(
    inside_settings->realm(), message_port_data);

// Expose the port to the worker's global object
global_scope->set_implicit_port(implicit_port);

```

This enables the standard `worker.postMessage()` and `self.onmessage` patterns to function across process boundaries, with IPC handling the underlying serialization and transfer semantics.

## Key Source Files and Implementation Details

The Web Worker implementation spans multiple libraries and services within the Ladybird codebase:

| Component | Responsibility | Key Source |
|-----------|----------------|------------|
| **Process launch helper** | Spawns `WebWorker` executable with type flags | [[`Libraries/LibWebView/HelperProcess.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/HelperProcess.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWebView/HelperProcess.cpp#L85) |
| **Worker service entry** | Main entry point for worker process | [[`Services/WebWorker/main.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/main.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/main.cpp) |
| **IPC connection (worker side)** | Receives `start_worker` and `close_worker` commands | [[`Services/WebWorker/ConnectionFromClient.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/ConnectionFromClient.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/ConnectionFromClient.cpp) |
| **Worker lifecycle** | Implements *run a worker* algorithm, realm creation, script fetch | [[`Services/WebWorker/WorkerHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/WorkerHost.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/WorkerHost.cpp) |
| **Minimal page host** | Supplies `Web::Page` for fetch/timers inside worker | [[`Services/WebWorker/PageHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/PageHost.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/PageHost.cpp) |
| **Browser-side IPC client** | UI process endpoint for worker requests | [`Libraries/LibWeb/Worker/WebWorkerClient.{h,cpp}`](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWeb/Worker/WebWorkerClient.h) |
| **Dedicated worker scope** | Global object for dedicated workers | [`Libraries/LibWeb/HTML/DedicatedWorkerGlobalScope.{h,cpp}`](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWeb/HTML/DedicatedWorkerGlobalScope.h) |
| **Shared worker scope** | Global object for shared workers | [`Libraries/LibWeb/HTML/SharedWorkerGlobalScope.{h,cpp}`](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.h) |
| **Environment settings** | Worker-specific environment settings object | [`Libraries/LibWeb/HTML/WorkerEnvironmentSettingsObject.{h,cpp}`](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWeb/HTML/WorkerEnvironmentSettingsObject.h) |
| **Fetch integration** | Script fetching for workers | [[`Libraries/LibWeb/HTML/Fetching.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/Fetching.cpp)](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWeb/HTML/Fetching.cpp) |

## Summary

Ladybird Browser’s Web Worker implementation delivers spec-compliant worker support through a **process-per-worker** architecture that prioritizes isolation and stability. The key architectural decisions include:

- **OS Process Isolation**: Each worker runs in a separate `WebWorker` process spawned via `HelperProcess::launch_web_worker_process`, ensuring that worker scripts cannot crash or block the main browser UI.
- **Spec-Compliant Lifecycle**: The `WorkerHost` class implements the HTML Living Standard's *run a worker* algorithm, handling realm creation, `EnvironmentSettingsObject` setup, and initial script fetching through the standard Fetch infrastructure.
- **Type-Specific Global Scopes**: Ladybird distinguishes between `DedicatedWorkerGlobalScope` and `SharedWorkerGlobalScope` at allocation time, ensuring that each worker type exposes the correct JavaScript APIs (`postMessage`, `importScripts`, `onconnect`).
- **Robust IPC Layer**: Communication between the UI process and worker processes flows through `WebWorkerClient` and `ConnectionFromClient`, with `MessagePort` objects handling the standard `postMessage` API across process boundaries.

## Frequently Asked Questions

### Does Ladybird use threads or processes for Web Workers?

Ladybird uses **separate OS processes** rather than threads for Web Workers. When JavaScript code creates a worker via `new Worker()`, the browser calls `HelperProcess::launch_web_worker_process` in [`Libraries/LibWebView/HelperProcess.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/HelperProcess.cpp) to spawn a new `WebWorker` executable. This process-per-worker model provides stronger isolation than thread-based approaches, ensuring that memory corruption or infinite loops in worker code cannot affect the main browser process or other workers.

### How does Ladybird handle SharedWorker compared to DedicatedWorker?

Ladybird distinguishes between worker types during global scope allocation in `WorkerHost::run`. For Dedicated Workers, the code allocates `DedicatedWorkerGlobalScope`, while Shared Workers receive `SharedWorkerGlobalScope`. This occurs in [`Services/WebWorker/WorkerHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/WorkerHost.cpp) within the lambda passed to `Bindings::create_a_new_javascript_realm`. The `is_shared` boolean flag, passed during process initialization via the `--type` command-line argument, determines which global scope class is instantiated, ensuring that Shared Workers expose the `onconnect` event while Dedicated Workers use the standard `postMessage` API.

### What security isolation does the process-per-worker model provide?

The process-per-worker architecture provides **memory isolation** and **failure containment** at the OS level. Since each worker runs in a separate address space via the `WebWorker` process, buffer overflows or use-after-free bugs in worker JavaScript cannot corrupt the main browser's heap. Additionally, if a worker enters an infinite loop or triggers an unrecoverable error, the browser process can terminate the specific worker process via `ConnectionFromClient::close_worker` without affecting other workers or the UI thread. This aligns with the web security model's requirement that workers execute in isolated contexts.

### How does script fetching work in Ladybird's WebWorker implementation?

Script fetching follows the HTML Living Standard's *perform the fetch* hook implemented in `WorkerHost::run`. The code creates a fetch hook using `Web::HTML::create_perform_the_fetch_hook`, which invokes the standard Fetch algorithm (`Web::Fetch::Fetching::fetch`) to retrieve the worker script. This occurs in [`Services/WebWorker/WorkerHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebWorker/WorkerHost.cpp) around lines 34-98. The fetch operation respects the worker's `EnvironmentSettingsObject`, applying CSP policies, referrer policies, and CORS checks before executing the script. Once the fetch completes, the response body is evaluated within the worker's isolated JavaScript realm.