Startup probes are a critical component of Cloud Run services, ensuring that your application is fully initialized and ready to serve traffic before it starts receiving requests. This prevents your service from returning errors to users or downstream services when it’s still in the process of starting up.
Let’s look at a typical Cloud Run service deployment and how startup probes can be configured to manage this.
Imagine you have a Node.js application that needs to establish a database connection and load some initial configuration data before it can handle incoming HTTP requests.
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: my-nodejs-app
spec:
template:
spec:
containers:
- name: my-app
image: gcr.io/my-project/my-nodejs-app:latest
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 6
In this configuration:
initialDelaySeconds: 15: This gives your application 15 seconds to start up and get ready before the first probe is even sent. This is crucial for applications with longer initialization times.periodSeconds: 10: After the initial delay, Cloud Run will check the/health/startupendpoint every 10 seconds.failureThreshold: 6: The probe will be retried up to 6 times (meaning up to 60 seconds of checks after the initial delay) before Cloud Run considers the container to have failed its startup. If it fails after these checks, the container will be restarted.
The /health/startup endpoint in your Node.js application would look something like this:
// Express.js example
const express = require('express');
const app = express();
let isReady = false;
let dbConnection = null;
// Simulate database connection and configuration loading
async function initializeApp() {
console.log('Starting application initialization...');
await new Promise(resolve => setTimeout(resolve, 10000)); // Simulate 10 seconds of work
dbConnection = { /* mock connection */ };
console.log('Database connected and configuration loaded.');
isReady = true;
}
initializeApp().catch(err => {
console.error('Failed to initialize application:', err);
process.exit(1); // Exit if initialization fails
});
// Startup probe endpoint
app.get('/health/startup', (req, res) => {
if (isReady) {
console.log('Startup probe: Ready');
res.status(200).send('Ready');
} else {
console.log('Startup probe: Not ready yet');
res.status(503).send('Service Unavailable');
}
});
// Regular health check or readiness probe (optional, but good practice)
app.get('/health/ready', (req, res) => {
if (isReady && dbConnection) {
res.status(200).send('OK');
} else {
res.status(503).send('Service Unavailable');
}
});
// Example endpoint that requires readiness
app.get('/data', (req, res) => {
if (!isReady || !dbConnection) {
return res.status(503).send('Service not ready yet.');
}
res.json({ message: 'Here is your data!' });
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
The key here is that the /health/startup endpoint only returns a 200 OK status after isReady is set to true, which happens only after the simulated initialization work is complete. Until then, it returns 503 Service Unavailable. Cloud Run interprets the 503 as a sign that the container is not yet ready and will keep retrying the probe according to the specified periodSeconds and failureThreshold. Once it receives a 200, it considers the container ready to receive traffic.
The most surprising thing about startup probes is that they are not just about when your application starts listening on a port, but about whether it’s capable of processing requests correctly. A common misconception is that a simple HTTP server listening on 0.0.0.0:8080 is enough; however, if that server hasn’t established its database connections, loaded its configuration, or completed its asynchronous initialization tasks, it can’t actually serve requests successfully, even if the port is open. The startup probe provides a mechanism to signal this internal readiness.
When you deploy a new revision of your Cloud Run service, traffic is gradually shifted to it. If startup probes are configured, Cloud Run will hold back traffic from a new revision until its containers pass the startup probe. This prevents users from hitting a new revision that is still warming up and potentially returning errors.
The initialDelaySeconds is the most crucial parameter for applications with significant startup work. Without it, or with a value too low, the probe might be sent before your application has a chance to even begin its initialization, leading to premature restarts and a failure to deploy. Finding the right balance for initialDelaySeconds, periodSeconds, and failureThreshold is an iterative process based on your application’s observed startup time.
The next step after mastering startup probes is to understand the nuances of readiness probes and how they differ, particularly in long-running applications that might become unhealthy after startup.