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
- Lower `worker_threads`, or pass `None` so it defaults to `available_parallelism()` (min 4)
- Raise the process's thread/pid limits: `ulimit -u`, cgroup `pids.max`, container `--pids-limit`
- Never pass `Some(0)`; validate the value before calling the matcher constructor
- 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
- Never pass Some(0) as a thread count; treat it as invalid at your API boundary
- Raise pid/thread limits (ulimit -u, cgroup pids.max, docker --pids-limit) before starting many workers
- Fix thread leaks before instantiating nested thread pools; count live threads at startup
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
- exceeded maximum allocation size
- exceeded maximum length
- overflowed maximum capacity
- clone pty reader
- failed to parse uuid for local store status
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)