vectordotdev/vector · error

double thread initialization

Error message

double thread initialization

What it means

app::build_runtime() records the configured worker-thread count in the process-wide WORKER_THREADS static using compare_exchange(0, threads). The first successful build sets the global; any subsequent call in the same process finds it non-zero and panics with 'double thread initialization'. The guard exists because num_threads and downstream sizing (e.g. source sender buffer sizing) depend on a single, stable worker-thread count for the process lifetime.

Source

Thrown at src/app.rs:577

}

pub fn build_runtime(
    threads: Option<usize>,
    chunk_size_events: Option<NonZeroUsize>,
    thread_name: &str,
) -> Result<Runtime, ExitCode> {
    let mut rt_builder = runtime::Builder::new_multi_thread();
    rt_builder.max_blocking_threads(20_000);
    rt_builder.enable_all().thread_name(thread_name);

    let threads = threads.unwrap_or_else(crate::num_threads);
    if threads == 0 {
        error!("The `threads` argument must be greater or equal to 1.");
        return Err(exitcode::CONFIG);
    }
    WORKER_THREADS
        .compare_exchange(0, threads, Ordering::Acquire, Ordering::Relaxed)
        .unwrap_or_else(|_| panic!("double thread initialization"));
    rt_builder.worker_threads(threads);

    let chunk_size_events = chunk_size_events
        .map(NonZeroUsize::get)
        .unwrap_or(vector_lib::source_sender::DEFAULT_CHUNK_SIZE_EVENTS);

    let Some(source_sender_buffer_size) = threads.checked_mul(chunk_size_events) else {
        error!(
            "The `chunk_size_events` argument is too large for the configured number of threads."
        );
        return Err(exitcode::CONFIG);
    };
    let Some(ready_array_capacity) =
        chunk_size_events.checked_mul(crate::topology::builder::READY_ARRAY_CAPACITY_CHUNKS)
    else {
        error!("The `chunk_size_events` argument is too large.");
        return Err(exitcode::CONFIG);
    };

View on GitHub (pinned to 3708c39b12)

Solutions

  1. Build the runtime once per process; on reconfiguration, re-exec the binary or run a fresh process instead of calling build_runtime again
  2. In tests, share one runtime across cases or spawn one process per case
  3. If you only need a runtime (not Vector's global bookkeeping), construct tokio::runtime::Builder directly instead of app::build_runtime

Example fix

// before
let rt1 = build_runtime(Some(4), None, "v")?;
let rt2 = build_runtime(Some(4), None, "v")?; // panics: double thread initialization

// after
let rt = build_runtime(Some(4), None, "v")?; // build once, reuse
// restart via re-exec if a fresh runtime is truly needed
Defensive patterns

Strategy: validation

Validate before calling

static RT_BUILT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if RT_BUILT.swap(true, Ordering::AcqRel) {
    // already built once in this process; re-exec instead of calling build_runtime again
    std::process::exit(reexec_self());
}
let rt = build_runtime(threads, chunk_size_events, name)?;

Prevention

When it happens

Trigger: Calling app::build_runtime (or running Vector's CLI main) twice in one process - e.g. a test binary that builds a runtime per case, or an embedding that restarts the 'app' layer without exec'ing a new process. threads == 0 is handled separately with a config error; only the double-init path panics.

Common situations: Integration tests invoking CLI entry points multiple times; wrappers that re-run Vector in-process after a config change instead of exec-ing a fresh process.

Related errors


AI-assisted analysis of vectordotdev/vector@3708c39b12 (2026-08-20). Data as JSON: /api/errors/120a5d05ae579153. Report an issue: GitHub.