# How Rustlings Handles File Descriptor Limits During Parallel Checking

> Learn how Rustlings gracefully handles file descriptor limits during parallel checking by automatically falling back to sequential mode and preventing command crashes.

- Repository: [The Rust Programming Language/rustlings](https://github.com/rust-lang/rustlings)
- Tags: internals
- Published: 2026-03-05

---

**Rustlings automatically falls back to sequential checking when parallel compilation hits operating-system file descriptor limits, ensuring the `check` command completes without crashing even on resource-constrained systems.**

The `rustlings` CLI from the `rust-lang/rustlings` repository validates Rust learning exercises by spawning a worker thread pool to compile and run them in parallel. When the number of concurrent processes exceeds the operating system's open file descriptor (**FD**) limit, the tool detects the failure pattern and retries the remaining exercises sequentially. This resilience strategy ensures that users with low `ulimit` configurations can still verify all exercises without manual intervention.

## Parallel Checking Architecture in Rustlings

When you run `rustlings check`, the tool executes `AppState::check_all_exercises_impl` in [`src/app_state.rs`](https://github.com/rust-lang/rustlings/blob/main/src/app_state.rs) to validate every exercise. By default, it creates one worker thread per logical CPU core to maximize throughput.

### Worker Thread Pool Initialization

The implementation uses `std::thread::available_parallelism()` to determine the optimal thread count, falling back to a default value if the query fails. Each worker runs in a scoped thread that iterates over the exercise list:

```rust
// src/app_state.rs#L13-L22 (excerpt)
let n_threads = thread::available_parallelism()
    .map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());

for _ in 0..n_threads {
    thread::Builder::new()
        .spawn_scoped(s, move || { /* loop over exercises */ })
        .context("Failed to spawn a thread to check all exercises")?;
}

```

Each worker opens exercise source files, invokes the Rust compiler, and executes binaries, which consumes multiple file descriptors per parallel task.

## Detecting and Handling File Descriptor Limits

Rather than querying system limits upfront, Rustlings adopts a reactive approach. If any worker thread fails to complete an exercise, the main thread interprets the error (manifested as `CheckProgress::None` or `CheckProgress::Checking` states) as a potential **FD limit exhaustion** and triggers recovery.

### The Sequential Fallback Mechanism

The fallback logic resides in the main progress loop around lines 79-95 of [`src/app_state.rs`](https://github.com/rust-lang/rustlings/blob/main/src/app_state.rs):

```rust
// src/app_state.rs#L79-L95
match progresses[exercise_ind] {
    // …
    CheckProgress::None | CheckProgress::Checking => {
        // If we got an error while checking all exercises in parallel,
        // it could be because we exceeded the limit of open file descriptors.
        // Therefore, try running exercises with errors sequentially.
        let exercise = &self.exercises[exercise_ind];
        let success = exercise.run_exercise(None, &self.cmd_runner)?;
        // Update progress visualizer...
    }
}

```

When triggered, the main thread re-executes the failing exercise—and any others still pending—using the sequential `exercise.run_exercise()` method. This guarantees that **file descriptor limits** do not cause the entire `check` command to abort, as the sequential path consumes only one FD at a time.

## Practical Examples

### Running Normal Parallel Checks

Under standard operation with sufficient FD limits, Rustlings utilizes full parallelism:

```bash
$ rustlings check
Running all exercises to check that they aren't already solved...

```

Behind the scenes, `AppState::check_all_exercises_impl` distributes work across the CPU-count thread pool.

### Simulating File Descriptor Exhaustion

You can force the **sequential fallback** by artificially restricting open files before running the command:

```bash
$ ulimit -n 64
$ rustlings check
... (parallel checks start)
... error detected, falling back to sequential checks

```

When threads fail to spawn `rustc` child processes due to the low limit, Rustlings detects the error state and automatically retries the affected exercises one-by-one in the main thread, completing the verification without user-visible crashes.

## Key Source Files and Implementation Details

The **parallel checking** and **FD limit resilience** logic spans several core files:

- **[`src/app_state.rs`](https://github.com/rust-lang/rustlings/blob/main/src/app_state.rs)** — Contains `AppState::check_all_exercises_impl`, which creates the thread pool, monitors `CheckProgress` states, and implements the sequential fallback for FD limit errors.
- **[`src/dev/check.rs`](https://github.com/rust-lang/rustlings/blob/main/src/dev/check.rs)** — Handles auxiliary parallel checks for unsolved exercises using separate threads, though without the specific FD fallback logic found in [`app_state.rs`](https://github.com/rust-lang/rustlings/blob/main/app_state.rs).
- **[`src/run.rs`](https://github.com/rust-lang/rustlings/blob/main/src/run.rs)** — Implements `run_exercise()`, the single-exercise execution routine invoked by both parallel workers and the sequential fallback path.
- **[`Cargo.toml`](https://github.com/rust-lang/rustlings/blob/main/Cargo.toml)** — Declares standard library dependencies (`std::thread`) used for the parallelism primitives.

## Summary

- **Thread Pool Creation**: Rustlings spawns worker threads equal to logical CPU cores using `thread::available_parallelism()` and `spawn_scoped`.
- **FD Limit Detection**: The system does not pre-check limits; instead, it monitors for `None` or `Checking` progress states that indicate parallel execution failures.
- **Automatic Fallback**: Upon detecting potential **file descriptor limits**, the tool seamlessly switches to sequential processing for remaining exercises via `exercise.run_exercise()`.
- **Zero Configuration**: Users do not need to adjust settings; the fallback is transparent, ensuring the `check` command succeeds regardless of system `ulimit` constraints.

## Frequently Asked Questions

### How does Rustlings determine the number of parallel threads to use?

The tool queries `std::thread::available_parallelism()` to obtain the logical CPU count, defaulting to a hardcoded constant if the query fails. This value sets the size of the worker thread pool spawned in [`src/app_state.rs`](https://github.com/rust-lang/rustlings/blob/main/src/app_state.rs).

### What happens if only one exercise fails during parallel checking?

The **sequential fallback** applies to the failing exercise and all subsequent pending exercises. The main thread takes over execution for the remainder of the queue, ensuring that transient **FD limit** issues do not leave exercises unchecked.

### Does Rustlings query the system's file descriptor limit before spawning threads?

No. The implementation relies on observing the error-on-parallel-run pattern rather than making explicit system calls like `getrlimit`. The comment in `src/app_state.rs#L82-L84` explicitly states this design choice, assuming FD exhaustion when parallel checks return errors.

### Can users disable parallel checking to avoid file descriptor issues?

There is no configuration flag to disable parallelism. However, the automatic **sequential fallback** eliminates the need for manual intervention—the tool effectively disables parallelism for you when it detects resource constraints, completing the verification process reliably.