gitbutlerapp/gitbutler · error
failed to create tokio runtime
Error message
failed to create tokio runtime
What it means
`tokio::runtime::Runtime::new()` creates a multi-thread runtime with I/O and time drivers; it fails when the OS refuses resources the drivers need (epoll or timerfd creation under fd exhaustion, memory pressure) or in restricted sandboxes. run_async() executes the future on a dedicated thread with its own runtime; the inner `.expect` panics only that thread, and `.join()` converts the panic into an anyhow error ("thread panicked"), so callers see an Err rather than a process crash.
Source
Thrown at crates/gitbutler-user/src/api.rs:258
}
resp.json()
.await
.context("Failed to parse profile update response")
})
}
/// Execute an async future on a dedicated thread with its own Tokio runtime.
///
/// This keeps the crate's public API synchronous while still using async HTTP
/// internally, following the same pattern as `but-forge`.
fn run_async<F, T>(future: F) -> Result<T>
where
F: std::future::Future<Output = Result<T>> + Send + 'static,
T: Send + 'static,
{
std::thread::spawn(move || {
tokio::runtime::Runtime::new()
.expect("failed to create tokio runtime")
.block_on(future)
})
.join()
.map_err(|e| anyhow::anyhow!("thread panicked: {e:?}"))?
}
#[cfg(test)]
mod tests {
use super::api_url_override_from_env;
#[test]
fn prefers_backend_specific_override() {
let url = api_url_override_from_env(|key| match key {
"GITBUTLER_API_URL" => Some("https://backend.example.com".to_string()),
"PUBLIC_API_BASE_URL" => Some("https://frontend.example.com".to_string()),
_ => None,
});
View on GitHub (pinned to caf1f223d3)
Solutions
- Check fd usage (lsof -p <pid> | wc -l) and raise limits (ulimit -n, LimitNOFILE) if the cause is EMFILE
- Share one lazily-created runtime across calls instead of one per call to cut resource churn
- Propagate instead of expect: build the runtime with .context("failed to create tokio runtime")? inside the thread and return Result
- In constrained environments use a single-threaded runtime (new_current_thread) or trim the driver set
Example fix
// before
std::thread::spawn(move || {
tokio::runtime::Runtime::new()
.expect("failed to create tokio runtime")
.block_on(future)
})
.join()
.map_err(|e| anyhow::anyhow!("thread panicked: {e:?}"))
// after
std::thread::spawn(move || -> Result<T> {
tokio::runtime::Runtime::new()
.context("failed to create tokio runtime")?
.block_on(future)
})
.join()
.map_err(|e| anyhow::anyhow!("thread panicked: {e:?}")) Defensive patterns
Strategy: try-catch
Validate before calling
// cheap pre-flight: can the process still allocate a file descriptor?
fn fds_available() -> bool {
std::fs::File::open("/dev/null").is_ok()
} Try / catch
match tokio::runtime::Runtime::new() {
Ok(rt) => rt.block_on(future),
Err(e) => Err(anyhow::Error::new(e).context("failed to create tokio runtime")),
} Prevention
- Reuse a global runtime for one-off async bridges
- Monitor open-file counts in long-lived processes
- Set a sane RLIMIT_NOFILE in packaged apps and service definitions
When it happens
Trigger: Any sync call into crates/gitbutler-user/src/api.rs reaching run_async() while the process is out of file descriptors (EMFILE) or memory: Runtime::new() inside the spawned thread fails, the thread panics, and join() maps the Box<dyn Any> payload into an anyhow error.
Common situations: A long-running desktop app with an fd leak from watchers or sockets; containers or launchd services with low RLIMIT_NOFILE; bursts of concurrent API calls each spawning a fresh thread plus runtime.
Related errors
- Failed to create runtime: {e}
- FATAL: Couldn't open in-memory URL: {path_err}
- panic while executing `{}` on thread '{}' ({}) at {}: {} pa
- panic while executing `{}`: {} panic backtrace unavailable
- Failed to create tokio runtime: {err}
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/b719735af047772e.
Report an issue: GitHub.