# Mocking a Health Check and Readiness Probe That Fails Until the Stub Is Ready

## Quick answer

When a mock server runs in a container or a compose stack, it should expose a health and readiness check just like a real service, and the check should distinguish "process started" from "stub ready to serve." A liveness probe that returns 200 the instant the process boots, before the spec is loaded and routes are registered, lets dependent clients send requests into a server that is not actually serving yet, producing connection resets or 404s that look like test flakiness. Model two phases: liveness that the process is up, and readiness that the contract has loaded and the mock can answer, then have orchestration wait on readiness before running tests.

## Why a single early 200 is misleading

A mock server does not become useful the moment its process starts. It has to read the spec, resolve any referenced examples, and register its routes, and that work takes a moment, especially when the spec is large or split across files.

If a client or a test runner starts hitting the mock between process start and route registration, the server is reachable but the expected endpoints are not there. Those failures are intermittent and depend on timing, which is exactly the kind of flakiness that erodes confidence in the suite.

## Liveness versus readiness

The two probes answer different questions and should not be merged into one endpoint. Liveness asks whether the process is running and should be restarted if it cannot answer; readiness asks whether the mock currently has its routes loaded and can accept traffic. A server that is alive but still loading should fail readiness without being killed, while a process that cannot even answer liveness should be restarted. Collapsing the two means either premature traffic or unnecessary restarts.

| Probe | Question it answers | Failure action |
| --- | --- | --- |
| Liveness | Is the process running? | Restart the container |
| Readiness | Is the spec loaded and routes registered? | Keep traffic out, wait |
| Startup probe | Has the slow initial load finished? | Give the container time before liveness |

## Implementing the readiness endpoint

Expose a health route that reports the internal state, and only return a success status once the mock has finished loading. A small example separates the two states.

In compose, point the healthcheck at the readiness route and use `depends_on` with a condition so client containers wait rather than racing ahead.

`services: mock: build: . healthcheck: test: ["CMD", "curl", "-f", "http://localhost:1080/health/ready"] interval: 3s retries: 10 tests: depends_on: mock: condition: service_healthy`

## Startup probes and grace windows

For a mock that loads a very large spec, the initial startup can exceed the normal liveness period, and a liveness probe that fails too early will restart a container that was doing legitimate work. A startup probe is the clean answer: it grants a longer grace window during boot, during which liveness and readiness are not yet enforced, and hands control to the normal probes only once the process has started. The key is to size the startup window from real measurements rather than guess, because a value that is too small restarts under load while one that is too large delays detection of a genuinely broken container. Even without a dedicated startup probe, configure a generous failure threshold on readiness so a slow but healthy load is not mistaken for a fault. Once the mock is up, readiness should still react quickly if the spec reloads or the route table drops, so the long grace window applies only to the first boot and not to the steady state. This distinction keeps a slow-loading mock both safe from premature restarts and honest about when it is genuinely unavailable.

## Handling reloads and failure to load

A mock that watches its spec and reloads on change needs to flip readiness back to not-ready during the reload, so tests do not hit a half-rebuilt route table. If the spec fails to parse, the readiness endpoint should report failure and expose the error rather than silently staying ready with stale routes. That makes a broken contract a visible, early failure instead of a mysterious set of 404s. Treat the mock's own startup as part of the system under test, because a suite is only reliable if the server it depends on is providably ready.

## FAQ

### Why not just add a sleep before the tests?

A fixed sleep is a guess. It is either too short on a slow machine or wasted time on a fast one, and it proves nothing about the server's state. A readiness check waits exactly until the mock can serve, which is deterministic and faster on average.

### What if the mock image has no health endpoint?

Wrap the process with a small sidecar or add a health route in the image, and avoid relying on a TCP connect check, which only proves the port is open, not that the routes are registered.

### Should readiness check the upstream services too?

For a standalone mock, no; it is meant to replace upstreams. Readiness should reflect the mock's own loaded state. If the mock has a pass-through mode, that portion can be checked separately.

### What happens when the spec reloads mid-run?

Mark the mock not-ready during the reload and restore readiness once the new routes are registered, so tests never observe a partially rebuilt table. A failed reload should surface an error rather than leave stale routes in place.

This is part of the [Powerduck vs Mockoon: OpenAPI Mock Servers](https://www.powerduck.com/blog/powerduck-vs-mockoon/) — start there for the full picture of running contract mocks reliably in CI.

