A health check that returns 200 while the process is not actually able to serve is one of the most reliably confusing failures in container land, precisely because every signal you would normally trust looks fine. The pattern that has saved me is separating liveness from readiness and making readiness assert something real - model loaded, dependency reachable - rather than just proving the web framework can answer. For model-serving containers there is a second trap in the same area: cold start often exceeds the default grace period, so the task gets killed mid-warmup and the platform reports it as unhealthy rather than as slow. It cycles forever and the logs never show anything wrong, which matches what you describe exactly.
Since separating readiness from liveness solved the migration issue, I would preserve the startup timeline as a regression check: process started, model initialized, first usable response, then readiness enabled. That makes a later dependency or model change easier to diagnose than another unexplained unhealthy loop.
The readiness signal should also stop admitting new work during shutdown while existing work drains. Testing that transition alongside cold start would cover the other moment when an apparently healthy process should temporarily receive no new traffic.
A health check that returns 200 while the process is not actually able to serve is one of the most reliably confusing failures in container land, precisely because every signal you would normally trust looks fine. The pattern that has saved me is separating liveness from readiness and making readiness assert something real - model loaded, dependency reachable - rather than just proving the web framework can answer. For model-serving containers there is a second trap in the same area: cold start often exceeds the default grace period, so the task gets killed mid-warmup and the platform reports it as unhealthy rather than as slow. It cycles forever and the logs never show anything wrong, which matches what you describe exactly.