Skip to content

vLLM returns 500 on every request after the EngineCore process dies

The vLLM V1 API server survives while the separate EngineCore subprocess that owns the model does not. Once that child is gone the server keeps accepting connections and answers every one of them with an internal error until it is restarted, so an outage presents as a healthy-looking process serving nothing.

Quick answer

EngineDeadError names the discovery, not the cause. Read the first worker or engine-core traceback printed ABOVE it, and check dmesg for an out-of-memory kill, which leaves no traceback at all.

Reliability#vllm#enginedeaderror#engine-core#inference-serving#outage#v1-engine

What this failure is

An outage mode specific to vLLM's split-process design, in which the model-owning EngineCore subprocess terminates while the HTTP server that fronts it continues running and answering requests with internal errors.

Why it happens (the mechanism)

vLLM V1 puts the model in its own process so the HTTP layer stays responsive. The consequence is that the two can fail independently. When the child dies, the parent finds out only when it next awaits output, and all it can honestly say is that the engine is gone. Nothing in that message distinguishes a missing CUDA kernel from a stalled worker from a subprocess reaped by the kernel, because the parent never saw any of them happen.

What you'll observe

  • Every completion request returns HTTP 500 while the container stays up and the port stays open
  • A liveness probe that only checks the port keeps the dead instance in the load balancer
  • The error the server reports names no cause, so there is nothing obvious to act on
  • Restarting clears it for a while and it returns under the same conditions

Common symptoms and what they mean

SymptomWhy it happens
vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.EngineDeadError is a bereavement notice, not a diagnosis. The API server process discovers the child is gone when it next awaits output, and reports that discovery. The exception that actually killed the engine was raised inside the EngineCore subprocess and printed to its own stream earlier, which is what the message means by see stack trace above.
Engine core proc EngineCore_DP0 died unexpectedly, shutting down clientThe child dies for reasons that share nothing but their effect. Reported causes include kernels that do not exist for the device, where a quantized build raises CUDA error no kernel image is available for execution on the device on the first request; an execute_model remote call timing out against a stalled worker; the kernel out-of-memory killer reaping the subprocess under load; and shared-memory limits breaking worker startup.
Traceback through output_handler, await engine_core.get_output_async, core_client.py get_output_asyncThe failure is durable because nothing restarts the engine. The parent has no model of its own to fall back on, so it stays alive serving errors, and a port-based liveness probe cannot tell that apart from a working server.
AsyncEngineDeadError: Background loop has errored already on the older V0 engineEngineDeadError is a bereavement notice, not a diagnosis. The API server process discovers the child is gone when it next awaits output, and reports that discovery. The exception that actually killed the engine was raised inside the EngineCore subprocess and printed to its own stream earlier, which is what the message means by see stack trace above.
POST /v1/chat/completions HTTP/1.1 500 Internal Server Error repeating for every subsequent requestThe child dies for reasons that share nothing but their effect. Reported causes include kernels that do not exist for the device, where a quantized build raises CUDA error no kernel image is available for execution on the device on the first request; an execute_model remote call timing out against a stalled worker; the kernel out-of-memory killer reaping the subprocess under load; and shared-memory limits breaking worker startup.

Which systems are affected

  • vLLM V1, where the API server and EngineCore are separate processes
  • Containers whose health check tests only TCP reachability rather than a real completion
  • Deployments using VLLM_WORKER_MULTIPROC_METHOD=spawn
  • Any autoscaled fleet, where one dead replica silently absorbs its share of traffic

How to confirm this is the problem

Apply the following checklist to a small reproduction: each box below is a positive signal that you are looking at this exact failure rather than a sibling in the same taxonomy.

  • Search the log backwards from the EngineDeadError for the first traceback attributed to a worker or engine-core process; that is the cause and it will name a device, a memory limit or a timeout.
  • Run dmesg -T | tail and look for an out-of-memory kill naming a Python process at the time of the failure. A kill leaves no application traceback.
  • Issue a one-token completion against the instance. If it returns 500 while the port accepts connections, the engine is dead and the process needs restarting rather than investigating live.

Example training logs (fingerprint)

training.log (synthetic fingerprint)
ERROR: Engine core proc EngineCore_DP0 died unexpectedly, shutting down client.
vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
INFO:     10.0.4.19:52344 - "POST /v1/chat/completions HTTP/1.1" 500 Internal Server Error

Timestamps and exact values vary across runs, but the pattern. An info-level start, an early WARN, an ERROR carrying the symptom. Is the actual fingerprint you should alert on. The Denpex platform flags this combination automatically.

Root cause, fix & prevention, signed in

Sign up free to see why this failure really happens, the exact remediation steps, and the production-grade prevention pattern. You also get 3 free full diagnoses for your own training logs.

Sign up free. Unlock the full analysis

No credit card · 3 free diagnoses · Instant access

Why the recommended fix works

Reading upward finds the exception raised where the failure actually occurred, which names a device, a limit or a timeout and therefore points at a specific change. Replacing the liveness probe fixes the second half of the problem: it converts a permanent silent outage into a restart, so a cause you have not diagnosed yet still stops costing traffic while you diagnose it.

Code examples

typical reference pattern
// Typical pattern:
import torch.optim as optim
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_training_steps)
for step in range(num_training_steps):
    optimizer.zero_grad()
    loss = model(batch)
    loss.backward()
    optimizer.step()
    scheduler.step()

Adapt the snippet to your framework. The same pattern holds for PyTorch Lightning, Hugging Face Trainer, DeepSpeed, Megatron-LM, and vLLM training wrappers. Where the wrapper exposes a config flag (for examplelr_scheduler_type in Trainer), prefer the flag over the imperative API to keep the schedule declarative and reproducible.

Best practices by model family

Model / StackRecommendationNotes
Dies on the first requestCheck kernel support for the device and quantization formatA missing kernel image raises before any load-dependent condition can exist.
Dies under sustained loadCheck dmesg for an out-of-memory kill, then lower --max-num-seqsA kernel kill leaves no traceback, so the log alone looks like an unexplained death.
Dies during worker startup with tensor parallelismRaise container shared memory or use host IPCWorker IPC fails before the engine ever serves a request.
Any production deploymentLiveness probe issues a real completionA port check cannot distinguish a dead engine from a healthy one.

With the fix vs without the fix

DimensionWith the fixWithout the fix
What EngineDeadError tells youThat the child process is goneAssumed to be the root cause itself
Where the real error livesIn the EngineCore or worker output, above the messageAssumed to be absent because the message names no cause
Effect of a port-based health checkDead replica keeps receiving trafficAssumed the orchestrator will restart it

Real engineering notes

The most commonly misdiagnosed variant is the one with no Python traceback anywhere. When the kernel out-of-memory killer reaps the subprocess, there is nothing above the EngineDeadError to read, and the absence of a cause gets reported upstream as a vLLM bug. Check dmesg before opening an issue; host memory pressure from a large tokenizer batch or a co-located process is a frequent culprit and has nothing to do with GPU memory.

Visual fingerprint

Which process died, and which one answers you
  client  ->  API server process  ->  EngineCore process  ->  GPU
                   (alive)                 (dead)
                      |                       |
                      |                       +-- real traceback printed HERE, earlier
                      +-- EngineDeadError + HTTP 500 printed here, later
The API server outlives the EngineCore process. The exception that ended the engine is printed by the engine's own process before it exits, so it appears earlier in the log than the EngineDeadError the server reports afterwards.

Root cause, fix & prevention

Frequently asked questions

Twelve targeted questions that engineers and on-call staff most commonly ask about this failure.

The error says to see the stack trace above, but there is nothing above it.
That usually means the subprocess was killed rather than raising. Check dmesg for an out-of-memory kill; a kernel kill terminates the process without giving Python a chance to print anything.
Why does the server keep returning 500 instead of exiting?
The API server is a separate process and is still healthy. It has no model to serve with and no logic to restart the engine, so it reports the failure for every request until something restarts it.
It only happens under load. Is that a vLLM bug?
Usually it is memory. Check for an out-of-memory kill first, then reduce --max-num-seqs and --max-num-batched-tokens, which bound peak concurrency and activation memory.
How do I stop a dead instance from receiving traffic?
Make the liveness probe issue a real short completion instead of checking the port. A dead engine answers the port normally and fails the completion.

Don't just read the fix, diagnose your run

The encyclopedia tells you what went wrong. Denpex tells you what went wrong in YOUR training run. With your logs, your config, and your stack.