# How webtor-rs Manages Tor Circuits: Architecture and Implementation Guide

> Discover how webtor-rs manages Tor circuits using its three-tier architecture. Explore the implementation details in CircuitManager, Circuit, and TorClient for efficient stream isolation and pool maintenance.

- Repository: [igor53627/webtor-rs](https://github.com/igor53627/webtor-rs)
- Tags: architecture
- Published: 2026-03-04

---

**webtor-rs manages Tor circuits through a three-tier architecture where `Circuit` structs track individual circuit state, `CircuitManager` handles pool maintenance and stream isolation, and `TorClient` provides the high-level API, all implemented in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs) and [`src/client.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/client.rs).**

The webtor-rs repository provides a Rust-based Tor client implementation designed for WebAssembly environments. Understanding how webtor-rs manages Tor circuits is essential for developers building privacy-preserving applications that require fine-grained control over circuit creation, stream isolation, and lifecycle maintenance.

## Core Components of Circuit Management

### The Circuit Struct

The `Circuit` struct in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs) (lines 35-48) represents a single Tor circuit with the following fields:

```rust
pub struct Circuit {
    pub id: String,                     // Human-readable identifier
    pub status: CircuitStatus,          // Creating, Ready, Extending, Failed, Closed
    pub created_at: Instant,            // When the circuit was built
    pub last_used: Instant,             // Updated on every request
    pub relays: Vec<Relay>,             // Bridge, middle and exit relays
    pub(crate) internal_circuit: Option<Arc<ClientTunnel>>,
    pub isolation_key: Option<IsolationKey>, // Optional binding for stream isolation
    _private: (),
}

```

The status enum drives state checks through methods like `is_ready()`, `is_failed()`, and `is_closed()` (lines 26-33). The `Circuit::new()` constructor stamps the creation time and defaults the status to `Creating` (lines 63-76).

### The CircuitManager

The `CircuitManager` in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs) (lines 90-115) acts as the central pool controller:

```rust
pub struct CircuitManager {
    circuits: Arc<RwLock<Vec<Arc<RwLock<Circuit>>>>>,
    relay_manager: Arc<RwLock<RelayManager>>,
    channel: Arc<RwLock<Option<Arc<Channel>>>>,
    prebuild_in_progress: Arc<AtomicBool>,
}

```

Key responsibilities include:
- **Storage**: A thread-safe `Vec` of `Arc<RwLock<Circuit>>` allows concurrent async access.
- **Creation**: Building new circuits through `create_circuit_with_isolation()`.
- **Isolation**: Assigning circuits to specific isolation keys.
- **Maintenance**: Pre-building spares and cleaning up expired circuits.

### The TorClient API

The `TorClient` in [`src/client.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/client.rs) (lines 30-38) provides the high-level interface that delegates all circuit work to the `CircuitManager`. During construction (`TorClient::new`), the client initializes the manager and optionally creates an early circuit (lines 69-73).

## Circuit Creation and Stream Isolation

### Building a New Circuit

The `CircuitManager::create_circuit_with_isolation()` method (lines 117-180) implements the construction logic:

1. **Pick the bridge**: A Snowflake bridge serves as the first hop using a FAST handshake.
2. **Select middle and exit relays**: Uses `RelayManager::select_relay` with criteria excluding previously chosen relays.
3. **Build the `ClientTunnel`**: Via the Arti `Channel` using `new_tunnel`, `create_firsthop_fast`, and `extend` operations.
4. **Populate the `Circuit` struct**: Stores the three relays, sets `status = Ready`, and optionally binds an `IsolationKey` before adding to the pool.

The function returns an `Arc<RwLock<Circuit>>` for use by the rest of the library.

### Implementing Stream Isolation

The `CircuitManager::get_circuit_for_isolation_key()` method (lines 332-376) enforces isolation:

| Step | Action |
|------|--------|
| 1️⃣ | Search for a **ready** circuit already bound to the requested key. |
| 2️⃣ | If none exists, find a **ready, unassigned** circuit, bind it to the key, and reuse it. |
| 3️⃣ | Enforce `MAX_CIRCUITS_PER_ISOLATION_KEY`. If the limit is reached, reuse any existing circuit for that key. |
| 4️⃣ | If no suitable circuit exists, **create a new one** already bound to the key via `create_circuit_with_isolation`. |

All operations occur under the same lock scope to prevent race conditions where two concurrent requests might steal the same unassigned circuit.

## Lifecycle Management and Maintenance

### Pre-building Spare Circuits

The `maybe_prebuild_circuit()` method (lines 629-702) maintains pool health:

- **Trigger conditions**: Checks current pool size and average circuit age via `CircuitStatusInfo`.
- **Concurrency control**: Uses an `AtomicBool` (`prebuild_in_progress`) to guarantee only one pre-build runs simultaneously.
- **Background execution**: Spawns the creation task using `tokio::spawn` or `wasm_bindgen_futures::spawn_local` depending on the target platform.

This runs periodically or after successful requests based on the `circuit_update_interval` configuration.

### Circuit Cleanup and Status Monitoring

The `cleanup_circuits()` method (lines 704-755) removes:

- **Failed** circuits (status `Failed`).
- **Very old** circuits (age ≥ 1 hour).
- **Idle** circuits (unused > 10 minutes) while preserving at least one circuit in the pool.

The method iterates the vector, gathers indices to drop, then removes them in reverse order to maintain index stability.

The `get_circuit_status()` method (lines 390-421) aggregates counts (`ready_circuits`, `creating_circuits`, `failed_circuits`) and calculates average age, which `TorClient` uses to generate human-readable status strings via `get_circuit_status_string`.

## Practical Implementation Examples

### Creating a Client and Fetching a URL

```rust
use webtor::client::TorClient;
use webtor::config::TorClientOptions;

#[tokio::main]
async fn main() -> webtor::error::Result<()> {
    // Snowflake bridge URL – the default bridge type
    let options = TorClientOptions::new("wss://snowflake.torproject.net/".into())
        .with_create_circuit_early(true);

    let client = TorClient::new(options).await?;
    client.bootstrap().await?;                 // fetch consensus (optional)

    let resp = client.get("https://httpbin.org/ip").await?;
    println!("Response: {}", resp.body_as_string()?);
    client.close().await;
    Ok(())
}

```

**Key flow**: `TorClient::new` → `establish_channel` → `CircuitManager::create_circuit` → `client.get` → `Circuit::begin_stream`.

### Using Stream Isolation Per Domain

```rust
let client = TorClient::new(options).await?;
let isolation_key = webtor::isolation::IsolationKey::from_string("example.com");

// Get or create a circuit bound to this key
let circ = client
    .circuit_manager
    .read()
    .await
    .get_circuit_for_isolation_key(Some(isolation_key))
    .await?;

// Use the circuit to open a stream
let stream = circ.read().await.begin_stream("example.com", 80).await?;

```

The manager guarantees that two different isolation keys never share the same circuit path.

### Triggering Pre-build Manually

```rust
let manager = client.circuit_manager.read().await.clone();
manager.maybe_prebuild_circuit(10, std::time::Duration::from_secs(300)).await;

```

When the average circuit age exceeds the 5-minute threshold and the pool is below the maximum size, a spare circuit builds in the background.

## Summary

- **webtor-rs manages Tor circuits** through a layered architecture separating state (`Circuit`), pool logic (`CircuitManager`), and public API (`TorClient`).
- **Deterministic lifecycle** states (`Creating`, `Ready`, `Failed`, `Closed`) tracked in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs) enable reliable cleanup and status reporting.
- **Stream isolation** binds circuits to specific keys via `get_circuit_for_isolation_key()`, ensuring different origins never share paths while enforcing per-key limits.
- **Proactive maintenance** includes pre-building spare circuits when pools age and automatic cleanup of failed or idle circuits after timeouts.
- **WASM compatibility** throughout the stack allows the same circuit management logic to run in both native Rust and WebAssembly environments.

## Frequently Asked Questions

### How does webtor-rs ensure that different applications or domains don't share the same Tor circuit?

webtor-rs implements **stream isolation** through the `IsolationKey` type. When fetching content, the `CircuitManager::get_circuit_for_isolation_key()` method (lines 332-376 in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs)) either finds an existing circuit bound to that key, binds an unassigned ready circuit to the key, or creates a new circuit already associated with the key. The system enforces `MAX_CIRCUITS_PER_ISOLATION_KEY` to prevent resource exhaustion while guaranteeing that different keys never share the same circuit path.

### What triggers the creation of a new Tor circuit in webtor-rs?

Circuits are created through three primary mechanisms in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs). First, **explicit creation** via `create_circuit_with_isolation()` (lines 117-180) when no suitable circuit exists for a request. Second, **pre-building** via `maybe_prebuild_circuit()` (lines 629-702), which spawns background circuit creation when the pool's average age exceeds configured thresholds. Third, **early initialization** when `TorClient` is constructed with `with_create_circuit_early(true)`, establishing the first circuit during client bootstrap.

### How does webtor-rs handle failed or expired circuits?

The `CircuitManager` performs periodic cleanup through `cleanup_circuits()` (lines 704-755 in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs)). This method removes circuits with `Failed` status, circuits older than one hour, and circuits idle for more than ten minutes, while always preserving at least one circuit in the pool to prevent complete drainage. Additionally, the `get_circuit_status()` method (lines 390-421) continuously monitors circuit health, enabling `TorClient` to report aggregate status and trigger maintenance cycles based on the `circuit_update_interval` configuration.

### Can webtor-rs build circuits in the background to improve latency?

Yes, webtor-rs implements **proactive circuit pre-building** via `maybe_prebuild_circuit()` in [`src/circuit.rs`](https://github.com/igor53627/webtor-rs/blob/main/src/circuit.rs) (lines 629-702). When the circuit pool size falls below a threshold or the average circuit age exceeds a specified duration (default 300 seconds), the manager spawns a background task using `tokio::spawn` or `wasm_bindgen_futures::spawn_local` to construct a new circuit. An `AtomicBool` flag (`prebuild_in_progress`) prevents duplicate concurrent pre-builds, ensuring efficient resource utilization while maintaining a pool of fresh circuits ready for immediate use.