atuinsh/atuin · critical

creating threadpool failed

Error message

creating threadpool failed

What it means

`Worker::new` builds the rayon thread pool with `ThreadPoolBuilder::build().expect("creating threadpool failed")`. `build()` returns Err when the configured number of OS threads cannot be spawned - thread/pid limits (RLIMIT_NPROC, cgroup pids.max), memory exhaustion, or a zero thread count. Because it panics at matcher startup, it takes the whole process down.

Solutions

  1. Lower `worker_threads`, or pass `None` so it defaults to `available_parallelism()` (min 4)
  2. Raise the process's thread/pid limits: `ulimit -u`, cgroup `pids.max`, container `--pids-limit`
  3. Never pass `Some(0)`; validate the value before calling the matcher constructor
  4. Fix thread leaks elsewhere in the process before starting the matcher

Example fix

// before
let (pool, worker) = Worker::new(Some(0), config, notify, cols); // invalid

// after
let threads = std::thread::available_parallelism()
    .map_or(4, |n| n.get())
    .max(1);
let (pool, worker) = Worker::new(Some(threads), config, notify, cols);
Defensive patterns

Strategy: validation

Validate before calling

// Validate thread budget before constructing the matcher
let requested = worker_threads.unwrap_or_else(|| {
    std::thread::available_parallelism().map_or(4, |n| n.get())
});
if requested == 0 {
    return Err("worker_threads must be >= 1".into());
}
// sanity: leave room for the rest of the process
if requested > 4 * std::thread::available_parallelism().map_or(4, |n| n.get()) {
    return Err("worker_threads unreasonably high".into());
}

Prevention

When it happens

Trigger: Creating the matcher worker pool when the process is already at its thread limit (a thread leak elsewhere, nested rayon pools, a container `--pids-limit`); passing `Some(0)` as `worker_threads`.

Common situations: Containers/sandboxes with low pid limits; test harnesses spawning many nucleo instances in parallel; fork servers hitting RLIMIT_NPROC.

Related errors


AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19). Data as JSON: /api/errors/6fc63a5a01b242d9. Report an issue: GitHub.

Appendix: source

Thrown at crates/atuin-nucleo/src/worker.rs:82

    }

    pub(crate) fn set_scorer(&mut self, scorer: Option<Scorer<T>>) {
        self.scorer = scorer;
    }

    pub(crate) fn new(
        worker_threads: Option<usize>,
        config: Config,
        notify: Arc<dyn Fn() + Sync + Send>,
        cols: u32,
    ) -> (ThreadPool, Self) {
        let worker_threads = worker_threads
            .unwrap_or_else(|| std::thread::available_parallelism().map_or(4, |it| it.get()));
        let pool = rayon::ThreadPoolBuilder::new()
            .thread_name(|i| format!("nucleo worker {i}"))
            .num_threads(worker_threads)
            .build()
            .expect("creating threadpool failed");
        let matchers = (0..worker_threads)
            .map(|_| UnsafeCell::new(atuin_nucleo_matcher::Matcher::new(config.clone())))
            .collect();
        let worker = Worker {
            running: false,
            matchers: Matchers(matchers),
            last_snapshot: 0,
            matches: Vec::new(),
            // just a placeholder
            pattern: MultiPattern::new(cols as usize),
            sort_results: true,
            reverse_items: false,
            canceled: Arc::new(AtomicBool::new(false)),
            should_notify: Arc::new(AtomicBool::new(false)),
            was_canceled: false,
            notify,
            items: Arc::new(boxcar::Vec::with_capacity(2 * 1024, cols)),
            in_flight: Vec::with_capacity(64),

View on GitHub (pinned to 15fe1318f1)