astrid-runtime/astrid · error
failed to spawn hook epoch ticker
Error message
failed to spawn hook epoch ticker
What it means
`WasmHandler::new` spawns a background OS thread that sleeps 100ms at a time and calls `ticker_engine.increment_epoch()` to power epoch-based fuel/interruption of hook execution. It panics if `std::thread::Builder::spawn` fails (thread creation returned an error), because hooks would otherwise never be preempted.
Solutions
- Raise the thread/pid limits (`ulimit -u`, cgroup pids.max) in the deployment environment.
- Reduce concurrent handler/thread count so spawn succeeds.
- Return a `Result` from `new` instead of panicking, mapping the io::Error to a hook-init error.
- Consider a lazily started singleton ticker thread shared by all handlers to avoid per-handler spawns.
Example fix
// before
.spawn(move || { /* epoch loop */ })
.expect("failed to spawn hook epoch ticker");
// after
.spawn(move || { /* epoch loop */ })
.map_err(|e| HookError::TickerSpawn(e.to_string()))? Defensive patterns
Strategy: fallback
Validate before calling
// Rust: pre-check headroom before spawning the ticker
fn can_spawn_thread() -> bool {
std::thread::Builder::new().stack_size(64 * 1024).spawn(|| {}).map(|h| h.join().is_ok()).unwrap_or(false)
} Try / catch
// Rust: fail construction with context instead of panicking
let ticker = builder.spawn(loop_body)
.map_err(|e| HookError::TickerSpawn(e.to_string()))?; Prevention
- Share one epoch ticker thread across handlers instead of one per handler.
- Set sane cgroup pids.max / ulimit -u values in deployment manifests.
- Log thread counts and alert before hitting the OS thread limit.
- Convert thread-spawn failures into typed init errors so callers can degrade gracefully.
When it happens
Trigger: Calling `WasmHandler::new` when `thread::spawn` fails: OS resource exhaustion (hit pid/thread limits like `ulimit -u`, cgroup `pids.max`, or `EAGAIN` from `pthread_create`), or insufficient memory for a new thread stack.
Common situations: Running many handlers/threads in constrained containers with low `pids.max`, deep recursion elsewhere exhausting stack, or a process approaching its thread limit under load.
Understand the failure class
Background: "unsupported platform" / "not supported on this platform" errors: what they mean and how to fix them — this error's family across 47 libraries.
Related errors
- alice
- build_host_state
- describe WASM in system catalog
- durable capsule removal is unavailable on portable hosts
- empty page deserializes
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/fac34b0b355f5484.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-hooks/src/handler/wasm.rs:126
impl WasmHandler {
/// Create a new WASM handler.
#[must_use]
pub(crate) fn new(workspace_root: PathBuf) -> Self {
let engine = build_hook_engine();
// Spawn epoch ticker so that epoch deadlines on Store actually fire.
let epoch_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let stop_clone = epoch_stop.clone();
let ticker_engine = engine.clone();
let epoch_handle = std::thread::Builder::new()
.name("hook-epoch-ticker".into())
.spawn(move || {
while !stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(100));
ticker_engine.increment_epoch();
}
})
.expect("failed to spawn hook epoch ticker");
Self {
engine,
cached_components: Mutex::new(HashMap::new()),
config: WasmConfig::default(),
kv: None,
http_limits: resolve_http_limits(),
workspace_root,
epoch_stop,
epoch_handle: Some(epoch_handle),
}
}
/// Set the KV store for hook state persistence.
#[must_use]
pub(crate) fn with_kv(mut self, kv: ScopedKvStore) -> Self {
self.kv = Some(kv);
selfView on GitHub (pinned to affd8760f4)