How to Implement an API Readiness Endpoint
Brew #2·September 16, 2026·8 minutes read
An API readiness endpoint is a dedicated HTTP endpoint used by the surrounding infrastructure to determine whether an application can currently receive traffic.
Missing or incorrect readiness handling can cause:
- New instances to receive traffic before startup is complete.
- Unavailable instances to continue receiving traffic.
- Healthy instances to be removed from traffic because of optional dependency failures.
- Repeated readiness checks to overload failing dependencies.
The short answer
The most common way to implement an API readiness endpoint is to:
-
Keep track of whether the application is in a starting, ready, or shutting down state.
-
Expose a dedicated endpoint, such as
GET /ready, that returns:- HTTP
200 OKwhen the application is ready and its required dependencies are available. - HTTP
503 Service Unavailableotherwise.
- HTTP
-
Configure the surrounding infrastructure to periodically call the endpoint and route traffic only to applications that report themselves as ready.
Tracking application readiness
An easy way to keep track of the application state is to create a singleton with a state property that holds one of these three values:
STARTING: the application is loading its configuration, initializing required dependencies, and starting the HTTP server.READY: the required dependencies have been initialized and the HTTP server is listening for incoming requests.SHUTTING_DOWN: graceful shutdown has started and the application should no longer receive new requests.
export type AppState =
| 'STARTING'
| 'READY'
| 'SHUTTING_DOWN';
class AppLifecycle {
private state: AppState = 'STARTING';
isReady(): boolean {
return this.state === 'READY';
}
isShuttingDown(): boolean {
return this.state === 'SHUTTING_DOWN';
}
setState(state: AppState): void {
this.state = state;
}
}
export const lifecycle = new AppLifecycle();
Ideally, this module should be imported at the top level and injected into the lower layers of the application like any other dependency.
Implementing a readiness endpoint
In its simplest version, the readiness endpoint checks whether the application is ready and returns an HTTP 200 OK or 503 Service Unavailable accordingly.
app.get('/ready', (_req, res) => {
res.sendStatus(lifecycle.isReady() ? 200 : 503);
});
This endpoint should be mounted after lightweight observability middleware, such as request tracing and logging, but before authentication, validation, business logic, or middleware that may fail for reasons unrelated to readiness.
export default function initServer({ lifecycle }: Dependencies): Application {
const app = express();
// Request proxying
// Request tracing
// Request logging
// Readiness check
// ...
return app;
}
Choosing which dependencies affect readiness
In addition to checking the application state, the readiness endpoint should also verify the health of any dependency whose failure would prevent the application from serving traffic.
For each dependency, ask: If this dependency fails, should the application stop receiving all traffic?
For example:
- If PostgreSQL is used to process requests and the application cannot serve traffic without it, this dependency should be considered as required and the endpoint should respond with an HTTP
503 Service Unavailable. - If Redis is only used to cache responses and requests can still be served from PostgreSQL, this dependency should be considered as optional and the endpoint should respond with an HTTP
200 OK.
For shared external dependencies, also consider whether removing the application from traffic would actually improve availability, since a dependency failure affecting every instance could cause all of them to fail readiness at once.
Performing lightweight dependency checks
Since the readiness endpoint is meant to be called repeatedly by the infrastructure, dependency checks should be cheap enough to run frequently without putting unnecessary load on the application or its dependencies. Each check should therefore perform the smallest operation that confirms the dependency is usable without causing side effects, such as creating database records, publishing messages, or sending emails.
For example, an API that relies on a database instance accessed through Prisma can execute a lightweight SELECT 1 query and return HTTP 503 Service Unavailable if either the application isn't ready or the query fails.
app.get('/ready', async (_req, res) => {
if (!lifecycle.isReady()) {
res.sendStatus(503);
return;
}
try {
await prisma.$queryRaw`SELECT 1`;
} catch {
res.sendStatus(503);
return;
}
res.sendStatus(200);
});
Running independent checks concurrently
To prevent dependency checks from accumulating response time and causing the readiness endpoint to exceed the probe timeout, independent checks should run concurrently. Otherwise, the infrastructure may treat the slow readiness response as a failed check and remove the application from traffic even though both the server and its dependencies are healthy.
For example:
await Promise.all([
prisma.$queryRaw`SELECT 1`,
redis.ping(),
]);
Bounding dependency checks
In addition to running dependency checks concurrently, each check should have a short timeout so a slow or unresponsive dependency cannot prevent the readiness endpoint from responding to the surrounding infrastructure.
Whenever possible, you should use the timeout or cancellation mechanism provided by the dependency client, driver, protocol, or service, so the underlying operation is actually stopped when the deadline is reached.
For example, MySQL allows a timeout to be applied to an individual read-only SELECT statement through the MAX_EXECUTION_TIME hint.
await prisma.$queryRaw`
SELECT /*+ MAX_EXECUTION_TIME(500) */ 1
`;
When no such mechanism is available, you could also use a custom JavaScript timeout that limits how long the readiness endpoint waits. However, this doesn't necessarily stop the underlying operations, which may continue running after the readiness endpoint has already failed.
async function withTimeout<T>(
operation: Promise<T>,
timeoutMs: number
): Promise<T> {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error('Readiness check timed out')),
timeoutMs
);
});
try {
return await Promise.race([operation, timeout]);
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
}
}
await withTimeout(
Promise.all([
// Lightweight dependency checks
]),
500
);
Production considerations
Configuring the infrastructure
The infrastructure calling the readiness endpoint should be configured with values that match how quickly the application is expected to respond and recover.
At a minimum, configure:
- The probe interval: how often the endpoint is called.
- The probe timeout: how long each readiness check may take at most.
- The failure threshold: how many consecutive failures are required before the application is considered unavailable.
For example, Docker Compose can use the readiness endpoint as a health check by calling it every 10 seconds, waiting up to 1 second for a response, and marking the container as unhealthy after 3 consecutive failures:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/ready"]
interval: 10s
timeout: 1s
retries: 3
Note that this example assumes
curlis installed within the container and the application is listening on port3000.
Keeping the endpoint internal or minimal
Whenever possible, the readiness endpoint should be accessible only to the infrastructure that needs it. In practice, this usually means keeping it off the public API through internal routing, network rules, or a separate management port.
For example:
- In Docker, the health check can call the endpoint from inside the container, while a reverse proxy, network configuration, or separate management port prevents public access to it.
- In Kubernetes, the readiness probe can call the Pod directly without routing
/readythrough the public Ingress.
If the endpoint must be public, it should expose as little information as possible. In most cases, an HTTP 200 OK or 503 Service Unavailable is enough. Detailed information about failed dependencies, such as database names, connection errors, internal service names, or stack traces, should remain in logs, metrics, or a protected diagnostic endpoint.
Allowing for a startup grace period
Some applications need time to initialize their dependencies before they can receive traffic. During that period, the readiness endpoint will legitimately return an HTTP 503 Service Unavailable.
If the infrastructure counts these expected failures toward its failure threshold, the application may be marked as unhealthy before startup completes. When supported, you should configure a startup grace period during which failed checks don't count toward that threshold.
For example, Docker Compose can provide the container with 20 seconds to initialize before failed health checks start counting toward retries:
healthcheck:
# ...
start_period: 20s