Guide · Container Runtime

MCP Tools for Google Cloud Run — revision lifecycle, traffic splitting, and cold start

Google Cloud Run is Google's managed serverless container platform. It runs OCI containers without requiring cluster management — you deploy a container image, Cloud Run handles instance scaling from zero to thousands based on request load. MCP tools that manage Cloud Run services face three platform-specific patterns not present in persistent container runtimes: revision lifecycle (every deploy creates an immutable revision; the service routes traffic across one or more revisions simultaneously using traffic percentages), cold start semantics (container instances can be scaled to zero and must cold-start on the next request — the health probe for a scaled-to-zero service should not be interpreted as "the container is healthy and running"), and service vs revision vs instance hierarchy (health status exists at three distinct levels, each with different information — checking service readiness misses revision-level failures, and checking revision status misses instance-level cold start problems).

TL;DR

Use the Google Cloud Run Admin API v2 via @google-cloud/run npm package or REST calls authenticated with Application Default Credentials. A service's serving_status is SERVING or NOT_SERVING; a revision's conditions array includes a Ready condition that reflects whether the revision can accept traffic. Traffic splitting lives on the service's traffic array — each entry has a revision name and a percentage. For cold start monitoring: set a minInstances of 1 to keep at least one warm instance, or accept that AliveMCP's 60-second probe will itself be subject to cold start latency when instances scale to zero. Register your service's HTTPS URL with AliveMCP for continuous external monitoring.

Cloud Run resource hierarchy: service → revision → instance

Cloud Run organizes resources into three levels. Understanding all three is necessary for accurate MCP tool health reporting.

Service: the top-level resource with a stable URL (https://SERVICE-PROJECT.REGION.run.app). The service holds the current traffic configuration, which specifies what percentage of requests go to each revision. Services have an overall serving_status field.

Revision: an immutable snapshot of the service configuration — container image, CPU/memory limits, environment variables, scaling settings, startup/liveness probes. Every deploy creates a new revision. Revisions can be independently addressable (via direct URL for testing: https://REVISION-HASH-PROJECT.REGION.run.app) and can receive independent traffic percentages. A revision has a conditions array with one entry per condition type, each having a state (CONDITION_SUCCEEDED, CONDITION_FAILED, CONDITION_PENDING).

Instance: a running container instance handling requests. Instances are ephemeral — they are created on demand and destroyed when idle beyond the idle timeout. You cannot directly manage instances via the API (no create/stop). What you can do: check the current instance count via Cloud Monitoring metrics (run.googleapis.com/container/instance_count).

import { ServicesClient, RevisionsClient } from '@google-cloud/run';

const servicesClient = new ServicesClient();  // uses Application Default Credentials
const revisionsClient = new RevisionsClient();

// Get service status
async function getServiceStatus(project, region, serviceName) {
  const name = `projects/${project}/locations/${region}/services/${serviceName}`;
  const [service] = await servicesClient.getService({ name });

  return {
    name: service.name,
    uri: service.uri,                    // the stable HTTPS URL
    servingStatus: service.servingStatus, // SERVING, NOT_SERVING
    latestReadyRevision: service.latestReadyRevision,     // last revision that passed health checks
    latestCreatedRevision: service.latestCreatedRevision, // newest revision (may not be ready yet)
    traffic: service.traffic?.map(t => ({
      revision: t.revision,
      percent: t.percent,
      tag: t.tag,                        // named traffic tag for direct URL routing
    })),
    conditions: service.conditions?.map(c => ({
      type: c.type,
      state: c.state,
      message: c.message,
    })),
  };
}

Revision lifecycle and readiness conditions

When you deploy a new container image to Cloud Run, the platform creates a new revision and moves it through a readiness lifecycle before routing traffic to it. For MCP tools that deploy and then wait for readiness, understanding this lifecycle prevents false positives (reporting ready before the revision has passed health checks) and false negatives (timing out before the revision has had time to start).

A revision's readiness is represented by conditions in the conditions array:

// Wait for a revision to become ready
async function waitForRevision(project, region, revisionName, timeoutMs = 300_000) {
  const name = `projects/${project}/locations/${region}/revisions/${revisionName}`;
  const start = Date.now();

  while (Date.now() - start < timeoutMs) {
    const [revision] = await revisionsClient.getRevision({ name });

    const readyCondition = revision.conditions?.find(c => c.type === 'Ready');
    if (!readyCondition) {
      await new Promise(r => setTimeout(r, 5000));
      continue;
    }

    if (readyCondition.state === 'CONDITION_SUCCEEDED') {
      return { ready: true, revision: revision.name };
    }
    if (readyCondition.state === 'CONDITION_FAILED') {
      return {
        ready: false,
        reason: readyCondition.reason,
        message: readyCondition.message,
        // Check which sub-condition failed
        failedConditions: revision.conditions
          ?.filter(c => c.state === 'CONDITION_FAILED')
          .map(c => ({ type: c.type, message: c.message })),
      };
    }

    // Still pending — log progress
    const pendingCondition = revision.conditions?.find(c => c.state === 'CONDITION_PENDING');
    console.log(`Revision pending: ${pendingCondition?.type} — ${pendingCondition?.message}`);
    await new Promise(r => setTimeout(r, 5000));
  }
  throw new Error('Timeout waiting for revision readiness');
}

A revision that fails to become ready does not automatically roll back traffic to the previous revision. Traffic remains on whichever revisions were previously receiving it — the new revision simply never gets traffic. For MCP tools implementing deployments: after updating the service's traffic configuration, verify the new revision's Ready condition before reporting success. If the revision fails, update the service to pin traffic back to the previous revision explicitly.

Traffic splitting: gradual rollouts and revision pinning

Cloud Run supports traffic splitting at the service level — you can send N% of requests to revision A and (100-N)% to revision B simultaneously. This enables canary deployments, A/B testing, and gradual rollouts without external infrastructure.

import { UpdateServiceRequest } from '@google-cloud/run';

// Gradual rollout: 10% to new revision, 90% to previous
async function gradualRollout(project, region, serviceName, newRevision, pct = 10) {
  const name = `projects/${project}/locations/${region}/services/${serviceName}`;
  const [service] = await servicesClient.getService({ name });

  // Find current primary revision
  const currentTraffic = service.traffic || [];
  const current = currentTraffic.find(t => (t.percent || 0) > 0);

  const newTraffic = [
    { revision: newRevision, percent: pct },
    { revision: current?.revision, percent: 100 - pct },
  ].filter(t => t.revision);

  await servicesClient.updateService({
    service: {
      name,
      traffic: newTraffic,
    },
    updateMask: { paths: ['traffic'] },
  });

  return { traffic: newTraffic };
}

// Fully migrate to new revision (100% traffic)
async function fullMigration(project, region, serviceName, revision) {
  const name = `projects/${project}/locations/${region}/services/${serviceName}`;
  await servicesClient.updateService({
    service: {
      name,
      traffic: [{ revision, percent: 100 }],
    },
    updateMask: { paths: ['traffic'] },
  });
}

// Pin to latest ready revision (remove manual traffic pins)
async function pinToLatest(project, region, serviceName) {
  const name = `projects/${project}/locations/${region}/services/${serviceName}`;
  await servicesClient.updateService({
    service: {
      name,
      traffic: [{ type: 'TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST', percent: 100 }],
    },
    updateMask: { paths: ['traffic'] },
  });
}

Traffic splitting at the Cloud Run level is weighted round-robin — not session-affinity-based. An MCP server that maintains session state in memory (SSE sessions, in-flight tool calls) will break if traffic is split between revisions, because subsequent requests may land on a different revision with no knowledge of the existing session. For MCP servers with session state, either use external session storage (Redis, Cloud Memorystore) that is shared across revisions, or use Cloud Run's session affinity feature (sessionAffinity: true on the service) — which routes requests from the same client to the same revision using a hash of the client IP. Note that session affinity only helps within a single revision; if you are splitting traffic between revisions, there is no cross-revision session affinity.

Cold start and minimum instances: probe semantics and AliveMCP

Cloud Run scales to zero by default: when no requests have arrived for the idle timeout period (default 15 minutes), all container instances are terminated. The next request triggers a cold start — Cloud Run allocates a new instance, pulls the container layers from the registry (or uses a cached layer in the region), starts the container process, and waits for the startup probe to pass before routing the request. Cold starts typically add 1-5 seconds of latency, though this varies by image size and startup complexity.

Cold start has important implications for health monitoring:

AliveMCP probes will trigger cold starts when all instances are scaled to zero. AliveMCP probes every 60 seconds. If your Cloud Run service has no traffic between probes and scales to zero, each probe will be a cold start and will take 1-5 seconds. This is expected and should not be reported as a failure — but it means AliveMCP's response time chart will show periodic spikes of 1-5 seconds for services with low traffic. AliveMCP's probe timeout must be set above 10 seconds for scaled-to-zero services.

Set minInstances to eliminate cold starts. Cloud Run allows setting a minimum number of instances that are always warm (scaling.minInstanceCount in the revision config). Setting minInstances: 1 keeps one instance always running — no cold start on the first request after an idle period, at the cost of a small always-on billing cost. For MCP servers where cold start latency would cause MCP client timeouts, set minInstances: 1.

// Deploy a revision with minimum instances to avoid cold start
async function deployWithWarmInstances(project, region, serviceName, image) {
  const name = `projects/${project}/locations/${region}/services/${serviceName}`;

  await servicesClient.updateService({
    service: {
      name,
      template: {
        containers: [{ image }],
        scaling: {
          minInstanceCount: 1,   // always one warm instance
          maxInstanceCount: 10,  // scale up to 10 under load
        },
      },
    },
    updateMask: { paths: ['template'] },
  });
}

// Check current instance count via Cloud Monitoring API
// (cloud monitoring requires separate @google-cloud/monitoring client)
// Metric: run.googleapis.com/container/instance_count
// Labels: service_name, revision_name, state (active/idle/starting)
async function getInstanceCount(project, region, serviceName) {
  // instance_count metric returns current instance count by state
  // 'active' = processing a request, 'idle' = warm but not serving, 'starting' = cold start in progress
  // This requires the Monitoring API — not directly available in Cloud Run API
  return { note: 'Use Cloud Monitoring API for instance count metrics' };
}

Startup and liveness probes: configure correctly for MCP servers

Cloud Run supports startup and liveness probes on container revisions. These are analogous to Kubernetes probes. Configuring them correctly prevents Cloud Run from routing traffic to instances that are still initializing (startup probe) or from leaving unhealthy instances in the pool indefinitely (liveness probe).

Startup probe: Cloud Run will not route requests to a new instance until the startup probe succeeds. The probe runs repeatedly at periodSeconds intervals; if it fails more than failureThreshold times, the instance is terminated and a new one is started. For MCP servers: set the startup probe to check the MCP initialize endpoint (or a simpler /health HTTP endpoint) with a timeout above the server's typical startup time. A too-aggressive startup probe (short timeout, high failure threshold) causes instances to be killed during startup, creating a loop of cold starts.

Liveness probe: Cloud Run periodically probes running instances; failing instances are replaced. For MCP servers that can deadlock (e.g., an in-flight tool call that blocks the event loop), a liveness probe on a health endpoint that only responds when the event loop is unblocked will cause Cloud Run to replace deadlocked instances automatically. Note that liveness probe failures terminate the instance abruptly — any in-flight MCP sessions are dropped. Design MCP servers to complete or reject in-flight requests within the instance's graceful shutdown period (terminationGracePeriodSeconds) before the probe kills the instance.

// Example revision configuration with startup and liveness probes
const revisionTemplate = {
  containers: [{
    image: 'us-docker.pkg.dev/myproject/my-mcp-server:latest',
    ports: [{ containerPort: 8080 }],
    startupProbe: {
      httpGet: { path: '/health', port: 8080 },
      initialDelaySeconds: 0,  // start probing immediately
      periodSeconds: 5,
      failureThreshold: 12,    // 12 * 5s = 60s total startup time budget
      timeoutSeconds: 3,
    },
    livenessProbe: {
      httpGet: { path: '/health', port: 8080 },
      periodSeconds: 30,
      failureThreshold: 3,     // 3 consecutive failures → replace instance
      timeoutSeconds: 5,
    },
    resources: {
      limits: { cpu: '1', memory: '512Mi' }
    },
  }],
  scaling: { minInstanceCount: 1, maxInstanceCount: 10 },
  maxInstanceRequestConcurrency: 80,  // requests per instance before scaling
};

Frequently asked questions

How do I deploy to Cloud Run from an MCP tool?

Use the Cloud Run Admin API v2 updateService method with an updated template.containers[0].image pointing to the new image tag. The minimal deploy flow: (1) Call updateService with the new image digest (us-docker.pkg.dev/project/repo/image@sha256:...) — using digests rather than tags guarantees immutability. (2) The API returns a long-running operation (LRO) object. (3) Poll the operation until it is done: operation.isDone(). (4) When the operation completes, the new revision has been created and the service's latestCreatedRevision is updated. (5) Wait for the revision's Ready condition to become CONDITION_SUCCEEDED. (6) Cloud Run automatically routes traffic to the new revision if the service is configured with TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST; if the service uses explicit revision pins, update the traffic allocation separately. Always use image digests instead of tags in production deployments — mutable tags can cause the revision to reference a different image than intended if the tag is pushed again between the deploy request and the actual container pull.

What is the difference between Cloud Run and Cloud Run Jobs?

Cloud Run services are request-driven: they start container instances in response to HTTP requests and scale back to zero when idle. Each instance handles one or more concurrent requests. Cloud Run Jobs are task-driven: you trigger a job execution and Cloud Run runs a specified number of container instances to completion, each executing the job's task independently. Jobs have a maximum task count (up to 10,000 parallel tasks) and support retry policies. For MCP tools: use Cloud Run services for MCP server deployments (request-driven, long-lived sessions). Use Cloud Run Jobs for one-off or batch workloads triggered by MCP tools — data migration, model inference batch jobs, report generation, backup operations. Jobs are invoked via POST /v2/projects/{project}/locations/{region}/jobs/{job}:run and expose an execution resource with per-task status. Monitoring Cloud Run Jobs requires checking execution status (EXECUTION_SUCCEEDED, EXECUTION_FAILED) and per-task completion, not the service serving_status or revision readiness used for services.

How do Cloud Run environment variables and secrets compare to other runtimes?

Cloud Run supports two mechanisms for injecting configuration into containers: environment variables (specified in the revision template, visible in plain text via the API and Cloud Console) and Secret Manager secrets (referenced by name and version, mounted either as environment variables or as files at a path in the container filesystem). For MCP servers handling credentials, always use Secret Manager secrets rather than plain environment variables — they are versioned, auditable, and can be rotated without redeploying the service (when using the latest version reference, the new secret value is available to new instances after a Cloud Run deployment or instance restart). To reference a secret: in the revision template's containers[0].env array, use valueSource.secretKeyRef.secret and valueSource.secretKeyRef.version rather than the plain value field. The Cloud Run service account must have roles/secretmanager.secretAccessor on the specific secret. Compared to ECS (which uses task definition environment variables or Secrets Manager ARNs) and Kubernetes (which uses Secret resources), Cloud Run's Secret Manager integration is the most straightforward: one IAM permission grants the service access to read specific secrets by name.

How does Cloud Run handle concurrent requests within a single instance?

Cloud Run instances are configured with a maxInstanceRequestConcurrency setting (default 80 for HTTP/2, 1 for HTTP/1.1 in the deprecated first-generation). This is the maximum number of requests a single instance can handle simultaneously before Cloud Run scales up additional instances. For MCP servers: if each MCP session corresponds to one long-lived HTTP connection (SSE-based transport), a maxInstanceRequestConcurrency of 80 means one instance handles up to 80 concurrent MCP sessions. When the 81st session arrives, Cloud Run scales up a new instance. For stdio-based MCP servers or batch-style request/response (short-lived HTTP requests), the default concurrency is fine. For session-stateful MCP servers (where each session maintains in-memory state), increasing concurrency per instance reduces the number of instances needed but increases memory pressure — monitor per-instance memory usage via Cloud Monitoring to tune the concurrency setting appropriately.

How do I monitor Cloud Run services with AliveMCP?

Register your Cloud Run service's HTTPS URL (the stable https://SERVICE-PROJECT.REGION.run.app URL or your custom domain if one is mapped) with AliveMCP. AliveMCP will probe the URL every 60 seconds. For services that scale to zero, AliveMCP's probe will trigger cold starts — set the AliveMCP probe timeout to at least 15 seconds to avoid false failure reports during cold start. For services with minInstances: 1 (always warm), probe latency will be consistently low. Cloud Run automatically provides a globally-cached TLS certificate for the run.app domain — no certificate management needed for AliveMCP to use HTTPS probing. If your Cloud Run service requires authentication (IAM-restricted service), the AliveMCP probe will receive a 403 unless you either make the service public (allow unauthenticated invocations on a /health path) or provide an AliveMCP probe with a long-lived service account token. The simplest pattern: set the IAM policy to allow unauthenticated invocations only on the /health endpoint by using Cloud Endpoints or Cloud Load Balancing with path-based IAM bypass rules, or by building authentication into the application (require the Authorization header on all paths except /health).

Further reading

Know when your MCP server is down — before users do

AliveMCP probes your server's MCP endpoint every minute, detects protocol errors and transport failures, and pages you before users notice.

Start monitoring free