# How Lightpanda Manages Concurrent Connections and Threading

> Discover how Lightpanda manages concurrent connections and threading using libcurl multiplexing connection pools and atomic operations for efficient high-throughput concurrency.

- Repository: [Lightpanda/browser](https://github.com/lightpanda-io/browser)
- Tags: internals
- Published: 2026-03-14

---

**Lightpanda achieves high-throughput concurrency through a three-layer architecture combining libcurl multi-handle multiplexing, mutex-protected connection pools with wake-up pipes, and atomic compare-and-swap operations for thread capping.**

Lightpanda is a lightweight headless browser written in Zig, designed for automation workloads requiring extreme efficiency. Understanding how Lightpanda manages concurrent connections and threading is essential for optimizing scraping and CDP (Chrome DevTools Protocol) applications, as the architecture deliberately minimizes OS thread usage while maximizing parallel network I/O.

## HTTP Multiplexing with libcurl Multi-Handle

The foundation of Lightpanda's outbound networking is the **libcurl multi-interface**, which allows a single OS thread to drive thousands of concurrent HTTP transfers. This design eliminates the memory overhead of per-connection threads while maintaining full parallelism for network I/O.

In `src/network/http.zig`, the `Handles` struct wraps the libcurl *multi* handle and manages the lifecycle of concurrent transfers. Initialization configures connection limits per host to prevent overwhelming servers:

```zig
const multi = libcurl.curl_multi_init() orelse return error.FailedToInitializeMulti;
try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen());

```

The `perform` method drives the event loop by repeatedly calling `curl_multi_perform`, which returns the count of still-active transfers:

```zig
pub fn perform(self: *Handles) !c_int {
    var running: c_int = undefined;
    try libcurl.curl_multi_perform(self.multi, &running);
    return running;
}

```

To avoid busy-waiting, the `poll` method blocks efficiently using `curl_multi_poll`, which waits for socket activity across all managed connections:

```zig
pub fn poll(self: *Handles, extra_fds: []libcurl.CurlWaitFd, timeout_ms: c_int) !void {
    try libcurl.curl_multi_poll(self.multi, extra_fds, timeout_ms, null);
}

```

This single-threaded multiplexing approach ensures that HTTP connection overhead remains constant regardless of the number of concurrent transfers.

## Runtime Connection Pool and Thread Coordination

While the HTTP layer handles multiplexing, `src/network/Runtime.zig` manages the **connection object pool** and coordinates between worker threads and the main event loop using a combination of mutexes and pipe-based signaling.

The runtime maintains a slice of live connections protected by a `std.Thread.Mutex`:

```zig
conn_mutex: std.Thread.Mutex = .{},
connections: []net_http.Connection,

```

When worker threads modify the connection list—adding new requests or removing completed ones—they acquire the mutex, update the `available` linked list, and signal the main thread via a **wake-up pipe**:

```zig
// Wakeup pipe: workers write to [1], main thread polls [0]
wakeup_pipe: [2]posix.fd_t = .{ -1, -1 },

```

The main thread polls `wakeup_pipe[0]` alongside libcurl sockets, ensuring immediate response to new work without constant mutex contention. This pattern provides **thread-safe coordination** where the critical section remains minimal (just list manipulation) while the pipe offers a lock-free wake-up path.

## Server-Side Thread Pool with Atomic Capping

For inbound CDP and WebSocket connections, `src/Server.zig` implements a **thread pool with hard caps** to prevent resource exhaustion. The server uses atomic operations rather than locks to manage the active thread count, ensuring non-blocking thread creation.

The `active_threads` counter is an `std.atomic.Value(u32)`:

```zig
active_threads: std.atomic.Value(u32) = .init(0),

```

The `spawnWorker` function uses a **weak compare-and-swap (CAS) loop** to atomically increment the counter only when below the configured maximum. This handles concurrent spawn attempts without blocking:

```zig
var current = self.active_threads.load(.monotonic);
while (current < max_connections) {
    current = self.active_threads.cmpxchgWeak(current, current + 1,
        .monotonic, .monotonic) orelse break;
} else {
    return error.MaxThreadsReached;
}

```

On successful CAS, the server spawns the worker thread using `std.Thread.spawn`, which runs `runWorker` and automatically decrements the counter on exit via `fetchSub`. Graceful shutdown is achieved by polling the atomic counter until zero:

```zig
while (self.active_threads.load(.monotonic) > 0) {
    std.Thread.sleep(10 * std.time.ns_per_ms);
}

```

## Practical Implementation Examples

### Initializing the HTTP Multiplexer

To create a multi-handle capable of managing parallel downloads:

```zig
const Handles = @import("network/http.zig").Handles;

var handles = try Handles.init(app.config);
defer handles.deinit();

// Add a prepared Connection
try handles.add(&myConn);

// Drive the event loop
while (true) {
    const running = try handles.perform();
    if (running == 0) break;
    try handles.poll(&.{}, 1000);
}

```

### Setting Up the Runtime Connection Pool

For managing connection objects across threads:

```zig
const Runtime = @import("network/Runtime.zig");

var runtime = try Runtime.init(app.allocator, app.config);
defer Runtime.deinit(runtime);

// Workers can safely enqueue connections; the main thread wakes via pipe
try runtime.handleConnection(conn);

```

### Spawning CDP Server Workers

To accept WebSocket connections with automatic thread capping:

```zig
const Server = @import("Server.zig");

var server = try Server.init(app, .{ .address = .{ .any = 0 }, .port = 9222 });
defer server.deinit();

// Server automatically manages worker threads via spawnWorker CAS logic

```

## Summary

- **Single-threaded HTTP multiplexing**: The `Handles` struct in `src/network/http.zig` uses libcurl's multi-interface to drive thousands of concurrent transfers on one thread, polling with `curl_multi_poll` to avoid busy-waiting.
- **Mutex-protected pools**: `src/network/Runtime.zig` protects connection objects with `std.Thread.Mutex` and uses a wake-up pipe for efficient thread coordination, keeping critical sections minimal.
- **Atomic thread capping**: `src/Server.zig` limits CDP/WebSocket worker threads using `std.atomic.Value(u32)` and CAS loops in `spawnWorker`, preventing resource exhaustion while allowing lock-free concurrency checks.
- **Graceful shutdown**: The server joins threads by polling the atomic counter until zero, ensuring clean termination without zombie threads.

## Frequently Asked Questions

### How does Lightpanda handle thousands of HTTP connections without creating thousands of threads?

Lightpanda uses **libcurl's multi-handle interface** as implemented in `src/network/http.zig`. The `Handles` struct calls `curl_multi_perform` in a loop to drive all transfers on a single thread, using `curl_multi_poll` to wait efficiently for socket activity. This allows parallel network I/O without the memory overhead of per-connection OS threads.

### What mechanism prevents the server from spawning too many CDP connection threads?

The server in `src/Server.zig` tracks active threads using `std.atomic.Value(u32)`. The `spawnWorker` function employs a **weak CAS (compare-and-swap) loop** that only increments the counter if the current value is below `max_connections`. If the limit is reached, the function returns `error.MaxThreadsReached`, effectively capping concurrency without locks.

### How does the runtime coordinate between worker threads and the main event loop?

`src/network/Runtime.zig` uses a **wake-up pipe** alongside a `std.Thread.Mutex`. When workers modify the connection pool, they lock the mutex briefly to update the list, then write to `wakeup_pipe[1]`. The main thread polls `wakeup_pipe[0]` with the libcurl sockets, ensuring it wakes immediately to process new work while avoiding constant mutex contention.

### Can the HTTP multiplexing layer handle WebSocket traffic?

No, the libcurl multi-handle in `src/network/http.zig` is designed for HTTP/HTTPS outbound traffic. WebSocket connections for CDP are handled by the server-side thread pool in `src/Server.zig`, which spawns dedicated worker threads per connection using `std.Thread.spawn`, while WebSocket framing logic resides in `src/network/websocket.zig`.