opendatalab/MinerU · critical · RuntimeError

Local worker exited before becoming healthy

Error message

Local worker {server_id} exited before becoming healthy

What it means

RuntimeError raised in router.py by LocalWorker.wait_until_ready: the subprocess spawned for a local API worker terminated (process.poll() is not None) before its /health endpoint ever returned 200. The worker process died during startup — the most information-rich failure in the local-worker lifecycle because the subprocess's own stderr usually holds the root cause.

Solutions

  1. Reproduce the worker startup manually (the same command the router spawns) and read the traceback it prints — that is the real error.
  2. Fix the environment: reinstall dependencies (pip install -U mineru ...), verify CUDA/driver compatibility, free the port.
  3. Check dmesg/systemd logs for OOM kills if there is no traceback; raise memory limits or preload fewer models.
  4. Validate the install with the mineru CLI sanity commands before running the router.

Example fix

# before
router start -> RuntimeError: Local worker 1 exited before becoming healthy

# after (diagnose: run the worker command directly)
# python -m mineru.cli.fast_api --host 127.0.0.1 --port 8001
# -> ModuleNotFoundError: No module named 'magic_pdf'
# pip install -U mineru[core] && retry
Defensive patterns

Strategy: try-catch

Validate before calling

# smoke-test the worker command before letting the router spawn it
import subprocess, sys
r = subprocess.run([sys.executable, '-m', 'mineru.cli.fast_api', '--help'],
                   capture_output=True, text=True, timeout=60)
assert r.returncode == 0, r.stderr[-2000:]

Try / catch

try:
    await worker.wait_until_ready(client)
except RuntimeError as exc:
    if 'exited before becoming healthy' in str(exc):
        stderr = read_worker_log(worker)  # subprocess stderr holds the root cause
        raise RuntimeError(f'worker startup crash: {stderr[-2000:]}') from exc
    raise

Prevention

When it happens

Trigger: Worker process crashes on import (missing dependency, incompatible torch/CUDA); model files missing or corrupted so startup aborts; port already in use causing immediate exit; Python environment mismatch (worker spawned with a different interpreter); OOM kill during model preload.

Common situations: Fresh installs with incomplete dependencies; GPU driver/CUDA mismatches; two services contending for the same port; container memory limits killing the worker during model load; upgrading mineru leaving stale caches.

Related errors


AI-assisted analysis of opendatalab/MinerU@4fe4bde114 (2026-08-14). Data as JSON: /api/errors/9844e29f13b0c01f. Report an issue: GitHub.

Appendix: source

Thrown at mineru/cli/router.py:462

        self.process_group_id = self.process.pid

        try:
            await self.wait_until_ready(client)
        except Exception:
            self.stop()
            raise

    async def wait_until_ready(
        self,
        client: httpx.AsyncClient,
        timeout_seconds: float = LOCAL_API_STARTUP_TIMEOUT_SECONDS,
    ) -> None:
        assert self.base_url is not None
        deadline = asyncio.get_running_loop().time() + timeout_seconds
        last_error: str | None = None
        while asyncio.get_running_loop().time() < deadline:
            if self.process is not None and self.process.poll() is not None:
                raise RuntimeError(f"Local worker {self.server_id} exited before becoming healthy")
            try:
                response = await client.get(f"{self.base_url}{HEALTH_ENDPOINT}")
                if response.status_code == 200:
                    return
                last_error = response_detail(response)
            except httpx.HTTPError as exc:
                last_error = str(exc)
            await asyncio.sleep(TASK_STATUS_POLL_INTERVAL_SECONDS)

        message = f"Timed out waiting for local worker {self.server_id} to become healthy"
        if last_error:
            message = f"{message}: {last_error}"
        raise RuntimeError(message)

    async def restart(self, client: httpx.AsyncClient) -> None:
        self.stop()
        await self.start(client)

View on GitHub (pinned to 4fe4bde114)