rwf2/Rocket · error
create tokio runtime
Error message
create tokio runtime
What it means
rocket::local::blocking::Client runs requests on its own private multi-thread tokio runtime (one worker thread) built inside Client::_new; the .expect("create tokio runtime") panics if Builder::build() returns an error. Build errors come from the environment: thread spawn failure (pid/thread limit reached, thread-stack memory exhausted) or I/O-driver fd creation failure (fd limit reached, seccomp blocking epoll). Because every Client constructs a fresh runtime, large parallel test suites multiply runtime creation and can exhaust limits even when each individual test passes.
Source
Thrown at core/lib/src/local/blocking/client.rs:39
/// let rocket = rocket::build();
/// let client = Client::tracked(rocket).expect("valid rocket");
/// let response = client.post("/")
/// .body("Hello, world!")
/// .dispatch();
/// ```
pub struct Client {
pub(crate) inner: Option<asynchronous::Client>,
runtime: RefCell<tokio::runtime::Runtime>,
}
impl Client {
fn _new<P: Phase>(rocket: Rocket<P>, tracked: bool, secure: bool) -> Result<Client, Error> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.thread_name("rocket-local-client-worker-thread")
.worker_threads(1)
.enable_all()
.build()
.expect("create tokio runtime");
// Initialize the Rocket instance
let inner = Some(runtime.block_on(asynchronous::Client::_new(rocket, tracked, secure))?);
Ok(Self { inner, runtime: RefCell::new(runtime) })
}
// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)]
pub fn _test<T, F>(f: F) -> T
where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send
{
let client = Client::debug(crate::build()).unwrap();
let request = client.get("/");
let response = request.clone().dispatch();
f(&client, request, response)
}
#[inline(always)]View on GitHub (pinned to 3a54d079ae)
Solutions
- Cap test parallelism: run cargo test -- --test-threads=8 (or lower) so fewer runtimes exist at the same time.
- Raise the limits in the test environment: Docker --pids-limit, cgroup pids.max, ulimit -u and ulimit -n.
- Serialize runtime-heavy tests with a global Mutex guard so clients/runtimes are created one at a time, and construct one Client per test rather than per request.
- Inside async harnesses (#[tokio::test] or #[rocket::test]) use rocket::local::asynchronous::Client instead of the blocking client.
- Fix fd leaks (unclosed files/sockets) so descriptors remain available for epoll/eventfd.
Example fix
// before — each parallel test builds its own tokio runtime; under a
// pids-limited CI container the last tests panic: 'create tokio runtime'
#[test]
fn list_users() {
let client = Client::tracked(rocket()).unwrap();
assert_eq!(client.get("/users").dispatch().status(), Status::Ok);
}
// after — serialize client construction (and run cargo test with
// `--test-threads=8` plus a higher --pids-limit in CI)
use std::sync::Mutex;
static CLIENT_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn list_users() {
let _guard = CLIENT_LOCK.lock().unwrap();
let client = Client::tracked(rocket()).unwrap();
assert_eq!(client.get("/users").dispatch().status(), Status::Ok);
} Defensive patterns
Strategy: validation
Validate before calling
fn can_build_runtime() -> bool {
// probe exactly what Client::_new does: build a throwaway runtime
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.is_ok()
}
// in the test harness before constructing blocking clients:
if !can_build_runtime() {
eprintln!("skipping: no thread/fd budget for another tokio runtime");
return;
} Try / catch
use std::panic::AssertUnwindSafe;
let client = std::panic::catch_unwind(AssertUnwindSafe(|| {
Client::tracked(rocket())
}));
match client {
Ok(c) => { /* proceed with requests */ }
Err(_) => panic!(
"environment cannot create a tokio runtime: check pids/fd limits"
),
} Prevention
- Cap cargo test parallelism (--test-threads) in pids-limited CI containers
- Raise --pids-limit, ulimit -u and ulimit -n in CI images
- Construct one Client per test, never inside request loops
- Use rocket::local::asynchronous::Client inside async test harnesses
- Fix fd leaks — each new runtime needs spare descriptors for epoll/eventfd
When it happens
Trigger: Calling Client::tracked(rocket), Client::untracked(rocket), or Client::debug(rocket) — all route through _new — when the test process is out of thread or fd budget; classically a large cargo test suite in a pids-limited container where each parallel test creates one more runtime with its own threads; EMFILE from leaked sockets/files leaving no descriptor for epoll/eventfd; seccomp/gVisor sandboxes in CI blocking the clone or epoll syscalls the runtime needs.
Common situations: Dockerized CI where cargo test defaults to high parallelism and hits --pids-limit; macOS/Linux dev machines or runners with low ulimit -u or ulimit -n during long test runs; leaked file descriptors in the code under test; tests mixing #[tokio::test] with the blocking client, which stresses the same resource budget; sandboxed CI executors.
Related errors
AI-assisted analysis of rwf2/Rocket@3a54d079ae (2026-08-16).
Data as JSON: /api/errors/577e95d604df0875.
Report an issue: GitHub.