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
- Build the runtime once per process; on reconfiguration, re-exec the binary or run a fresh process instead of calling build_runtime again
- In tests, share one runtime across cases or spawn one process per case
- 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
- Build Vector's runtime once per process; restart by re-exec, not by re-calling build_runtime
- Share one runtime across test cases or run cases in separate processes
- Use plain tokio::runtime::Builder for non-Vector runtimes to avoid the global thread-count guard
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
- double chunk_size_events initialization
- max_decompressed_size_bytes already set
- max_zlib_compressed_frame_size_bytes already set
- max_zstd_window_log already set
- path and query should never fail to parse
AI-assisted analysis of vectordotdev/vector@3708c39b12 (2026-08-20).
Data as JSON: /api/errors/120a5d05ae579153.
Report an issue: GitHub.