A Beginner's Guide to Writing Your First Multi-Agent Pipeline Health Check Endpoint

A Beginner's Guide to Writing Your First Multi-Agent Pipeline Health Check Endpoint

You've built your first multi-agent pipeline. Maybe it's a research agent that hands off summaries to a writer agent, which then passes formatted output to a publisher agent. It works beautifully in your local environment. Then you deploy it, walk away, and come back to find it silently broken at 2 AM with zero visibility into what went wrong.

This is the silent failure problem that plagues most early multi-agent systems. Unlike a traditional REST API where a 500 status code screams for attention, agent pipelines fail in subtle, expensive ways: an agent stalls waiting for a dependency that timed out, a token budget gets exhausted mid-pipeline, or a downstream tool becomes unavailable while the orchestrator keeps retrying indefinitely.

The fix is surprisingly simple: a single, well-designed health check endpoint that gives you a real-time snapshot of every agent's status, every dependency's availability, and your remaining token budget, all in one API response. In this guide, you'll learn exactly how to build one from scratch, even if you've never written a monitoring endpoint before.

Why Standard Health Checks Fall Short for Agent Pipelines

Traditional health check endpoints answer one question: "Is the server running?" They return a simple 200 OK with maybe a {"status": "ok"} body. That's fine for a stateless microservice, but multi-agent pipelines are anything but stateless.

Consider what a meaningful health check actually needs to capture in a multi-agent context:

  • Agent-level status: Is each individual agent idle, running, waiting, or in a failed state?
  • Dependency availability: Are the external tools, vector databases, APIs, and model providers that agents rely on actually reachable?
  • Token budget remaining: How much of your allocated token budget has been consumed, and is there enough left to complete pending work?
  • Pipeline topology: Which agents are blocked waiting on upstream agents?
  • Error context: If something failed, what failed and why?

A simple ping-style health check misses all of this. By the time your monitoring system flags the server as "down," your pipeline has likely already burned thousands of tokens and produced corrupted output. The goal is to catch problems before they become catastrophic failures.

Designing the Health Check Response Schema

Before writing a single line of code, design your response schema. A well-structured schema is the foundation of a useful health check. Here's a schema that covers all the essentials for a beginner-level implementation:

{
  "status": "healthy" | "degraded" | "unhealthy",
  "timestamp": "2026-06-15T10:32:00Z",
  "pipeline": {
    "id": "research-writer-pipeline",
    "version": "1.0.0",
    "uptime_seconds": 3842
  },
  "agents": [
    {
      "id": "research-agent",
      "status": "idle" | "running" | "waiting" | "failed",
      "last_active": "2026-06-15T10:31:55Z",
      "current_task": null,
      "error": null
    }
  ],
  "dependencies": [
    {
      "name": "openai-api",
      "type": "model_provider",
      "status": "available" | "degraded" | "unavailable",
      "latency_ms": 124,
      "last_checked": "2026-06-15T10:31:58Z"
    }
  ],
  "token_budget": {
    "total_allocated": 500000,
    "consumed": 187432,
    "remaining": 312568,
    "percent_used": 37.5,
    "burn_rate_per_minute": 4200,
    "estimated_minutes_remaining": 74
  }
}

Notice the top-level status field. This is your rollup status: the single value your monitoring system will alert on. The rule is simple:

  • healthy: All agents are idle or running normally, all dependencies are available, and the token budget is above a safe threshold (for example, more than 20% remaining).
  • degraded: At least one dependency is slow or intermittent, or a non-critical agent has failed, but the pipeline can still make progress.
  • unhealthy: A critical agent has failed, a required dependency is down, or the token budget is critically low (under 5% remaining).

Setting Up the Project Structure

For this guide, we'll use Python with FastAPI, which is the most common framework for building AI agent backends in 2026. You'll also need a lightweight way to track agent state. We'll use a simple in-memory state store to keep things approachable.

Install the dependencies:

pip install fastapi uvicorn httpx python-dotenv

Here's the project layout we'll build toward:

my_agent_pipeline/
├── main.py
├── agents/
│   ├── __init__.py
│   ├── registry.py       # Agent state registry
│   └── base_agent.py     # Base agent class
├── health/
│   ├── __init__.py
│   ├── router.py         # Health check endpoint
│   ├── checks.py         # Individual check functions
│   └── models.py         # Pydantic response models
└── config.py             # Token budget configuration

Step 1: Build the Agent State Registry

The registry is the source of truth for all agent statuses. Every agent in your pipeline reports its state to this central store, and the health check reads from it. Keep it simple to start.

In agents/registry.py:

from datetime import datetime, timezone
from typing import Dict, Optional
from dataclasses import dataclass, field
from enum import Enum


class AgentStatus(str, Enum):
    IDLE = "idle"
    RUNNING = "running"
    WAITING = "waiting"
    FAILED = "failed"


@dataclass
class AgentState:
    agent_id: str
    status: AgentStatus = AgentStatus.IDLE
    last_active: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    current_task: Optional[str] = None
    error: Optional[str] = None


class AgentRegistry:
    """Central store for all agent states in the pipeline."""

    def __init__(self):
        self._agents: Dict[str, AgentState] = {}

    def register(self, agent_id: str) -> None:
        """Register a new agent with the pipeline."""
        self._agents[agent_id] = AgentState(agent_id=agent_id)

    def update(
        self,
        agent_id: str,
        status: AgentStatus,
        current_task: Optional[str] = None,
        error: Optional[str] = None,
    ) -> None:
        """Update an agent's current state."""
        if agent_id not in self._agents:
            self.register(agent_id)
        state = self._agents[agent_id]
        state.status = status
        state.last_active = datetime.now(timezone.utc)
        state.current_task = current_task
        state.error = error

    def get_all(self) -> list[AgentState]:
        return list(self._agents.values())

    def get(self, agent_id: str) -> Optional[AgentState]:
        return self._agents.get(agent_id)


# Singleton instance shared across the application
agent_registry = AgentRegistry()

With this registry in place, any agent in your pipeline can report its status with a single call:

from agents.registry import agent_registry, AgentStatus

agent_registry.update("research-agent", AgentStatus.RUNNING, current_task="Fetching arxiv papers")

Step 2: Build the Token Budget Tracker

Token budget tracking is the feature most beginners skip, and it's the one that saves you the most money. The idea is straightforward: you allocate a token budget for a pipeline run (or for a rolling time window), track consumption as agents make model calls, and expose the remaining budget in your health check.

In config.py:

import time
from threading import Lock


class TokenBudget:
    """Thread-safe token budget tracker for the pipeline."""

    def __init__(self, total_allocated: int):
        self.total_allocated = total_allocated
        self._consumed = 0
        self._lock = Lock()
        self._start_time = time.time()
        self._consumption_history: list[tuple[float, int]] = []  # (timestamp, tokens)

    def consume(self, tokens: int) -> None:
        """Record token consumption from an agent call."""
        with self._lock:
            self._consumed += tokens
            self._consumption_history.append((time.time(), tokens))
            # Keep only the last 5 minutes of history for burn rate calculation
            cutoff = time.time() - 300
            self._consumption_history = [
                (t, v) for t, v in self._consumption_history if t >= cutoff
            ]

    @property
    def consumed(self) -> int:
        return self._consumed

    @property
    def remaining(self) -> int:
        return max(0, self.total_allocated - self._consumed)

    @property
    def percent_used(self) -> float:
        return round((self._consumed / self.total_allocated) * 100, 2)

    @property
    def burn_rate_per_minute(self) -> float:
        """Calculate tokens consumed per minute over the last 5 minutes."""
        if not self._consumption_history:
            return 0.0
        window_tokens = sum(v for _, v in self._consumption_history)
        elapsed_minutes = min(5.0, (time.time() - self._start_time) / 60)
        if elapsed_minutes == 0:
            return 0.0
        return round(window_tokens / elapsed_minutes, 2)

    @property
    def estimated_minutes_remaining(self) -> Optional[float]:
        rate = self.burn_rate_per_minute
        if rate == 0:
            return None
        return round(self.remaining / rate, 1)


# Initialize with your desired budget (adjust to your use case)
token_budget = TokenBudget(total_allocated=500_000)

Now, every time an agent makes a model call, it reports token usage:

from config import token_budget

# After receiving a response from your model provider
token_budget.consume(response.usage.total_tokens)

Step 3: Write the Dependency Checks

Dependency checks probe your external services and report their availability and latency. The key principle here is: keep each check fast and non-blocking. A health check that takes 10 seconds to respond because it's waiting on a slow dependency is worse than useless.

In health/checks.py:

import httpx
import asyncio
import time
from datetime import datetime, timezone
from typing import Optional


async def check_http_dependency(
    name: str,
    url: str,
    timeout_seconds: float = 3.0,
) -> dict:
    """Probe an HTTP-based dependency and return its status."""
    start = time.monotonic()
    last_checked = datetime.now(timezone.utc).isoformat()

    try:
        async with httpx.AsyncClient(timeout=timeout_seconds) as client:
            response = await client.get(url)
        latency_ms = round((time.monotonic() - start) * 1000, 2)

        if response.status_code < 500:
            status = "available" if latency_ms < 1000 else "degraded"
        else:
            status = "unavailable"

    except httpx.TimeoutException:
        latency_ms = round(timeout_seconds * 1000, 2)
        status = "unavailable"
    except Exception as e:
        latency_ms = None
        status = "unavailable"

    return {
        "name": name,
        "status": status,
        "latency_ms": latency_ms,
        "last_checked": last_checked,
    }


async def run_all_dependency_checks() -> list[dict]:
    """Run all dependency checks concurrently."""
    checks = await asyncio.gather(
        check_http_dependency(
            name="openai-api",
            url="https://api.openai.com/v1/models",
        ),
        check_http_dependency(
            name="vector-db",
            url="http://localhost:6333/healthz",  # Example: Qdrant health endpoint
        ),
        check_http_dependency(
            name="redis-cache",
            url="http://localhost:6379/ping",
        ),
        return_exceptions=True,
    )
    # Filter out any unexpected exceptions and replace with error entries
    return [
        c if isinstance(c, dict) else {"name": "unknown", "status": "unavailable", "latency_ms": None, "last_checked": datetime.now(timezone.utc).isoformat()}
        for c in checks
    ]

Notice the use of asyncio.gather. This runs all dependency checks in parallel, so if you have five dependencies each taking up to three seconds to time out, your health check still responds in roughly three seconds rather than fifteen. This is critical for keeping your monitoring system responsive.

Step 4: Assemble the Health Check Endpoint

Now bring everything together in the FastAPI router. This is where the rollup logic lives: computing the top-level status from all the individual checks.

In health/router.py:

from fastapi import APIRouter
from datetime import datetime, timezone
import time

from agents.registry import agent_registry, AgentStatus
from health.checks import run_all_dependency_checks
from config import token_budget

router = APIRouter()

# Track pipeline start time
_pipeline_start_time = time.time()


def compute_rollup_status(
    agent_states: list,
    dependency_results: list,
    budget_percent_remaining: float,
) -> str:
    """Determine the overall pipeline health status."""

    # Check for any failed agents
    failed_agents = [a for a in agent_states if a.status == AgentStatus.FAILED]
    unavailable_deps = [d for d in dependency_results if d["status"] == "unavailable"]
    degraded_deps = [d for d in dependency_results if d["status"] == "degraded"]

    # Critically low token budget
    if budget_percent_remaining < 5.0:
        return "unhealthy"

    # Any critical failure
    if failed_agents or unavailable_deps:
        return "unhealthy"

    # Degraded conditions
    if degraded_deps or budget_percent_remaining < 20.0:
        return "degraded"

    return "healthy"


@router.get("/health", tags=["Monitoring"])
async def health_check():
    """
    Returns a comprehensive health snapshot of the multi-agent pipeline,
    including agent statuses, dependency availability, and token budget.
    """
    # Gather all data concurrently where possible
    dependency_results = await run_all_dependency_checks()
    agent_states = agent_registry.get_all()
    uptime_seconds = int(time.time() - _pipeline_start_time)

    # Build the response
    agents_payload = [
        {
            "id": state.agent_id,
            "status": state.status.value,
            "last_active": state.last_active.isoformat(),
            "current_task": state.current_task,
            "error": state.error,
        }
        for state in agent_states
    ]

    budget_percent_remaining = round(
        100.0 - token_budget.percent_used, 2
    )

    rollup_status = compute_rollup_status(
        agent_states, dependency_results, budget_percent_remaining
    )

    return {
        "status": rollup_status,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "pipeline": {
            "id": "my-agent-pipeline",
            "version": "1.0.0",
            "uptime_seconds": uptime_seconds,
        },
        "agents": agents_payload,
        "dependencies": dependency_results,
        "token_budget": {
            "total_allocated": token_budget.total_allocated,
            "consumed": token_budget.consumed,
            "remaining": token_budget.remaining,
            "percent_used": token_budget.percent_used,
            "percent_remaining": budget_percent_remaining,
            "burn_rate_per_minute": token_budget.burn_rate_per_minute,
            "estimated_minutes_remaining": token_budget.estimated_minutes_remaining,
        },
    }

Step 5: Wire It Into Your FastAPI Application

In your main.py, register the agents and mount the health router:

from fastapi import FastAPI
from health.router import router as health_router
from agents.registry import agent_registry

app = FastAPI(title="My Multi-Agent Pipeline")

# Register your pipeline agents on startup
@app.on_event("startup")
async def startup():
    agent_registry.register("research-agent")
    agent_registry.register("writer-agent")
    agent_registry.register("publisher-agent")

# Mount the health check router
app.include_router(health_router)

# Your agent pipeline routes go here
# app.include_router(pipeline_router)

Run your app and visit http://localhost:8000/health to see your full pipeline health snapshot.

What a Real Response Looks Like

Here's an example of what the endpoint returns when the pipeline is in a degraded state due to a slow vector database and a low token budget:

{
  "status": "degraded",
  "timestamp": "2026-06-15T14:22:10Z",
  "pipeline": {
    "id": "my-agent-pipeline",
    "version": "1.0.0",
    "uptime_seconds": 7241
  },
  "agents": [
    {
      "id": "research-agent",
      "status": "idle",
      "last_active": "2026-06-15T14:22:05Z",
      "current_task": null,
      "error": null
    },
    {
      "id": "writer-agent",
      "status": "running",
      "last_active": "2026-06-15T14:22:09Z",
      "current_task": "Drafting section 3 of 5",
      "error": null
    },
    {
      "id": "publisher-agent",
      "status": "waiting",
      "last_active": "2026-06-15T14:21:50Z",
      "current_task": null,
      "error": null
    }
  ],
  "dependencies": [
    {
      "name": "openai-api",
      "status": "available",
      "latency_ms": 98,
      "last_checked": "2026-06-15T14:22:09Z"
    },
    {
      "name": "vector-db",
      "status": "degraded",
      "latency_ms": 1840,
      "last_checked": "2026-06-15T14:22:10Z"
    },
    {
      "name": "redis-cache",
      "status": "available",
      "latency_ms": 3,
      "last_checked": "2026-06-15T14:22:09Z"
    }
  ],
  "token_budget": {
    "total_allocated": 500000,
    "consumed": 412000,
    "remaining": 88000,
    "percent_used": 82.4,
    "percent_remaining": 17.6,
    "burn_rate_per_minute": 5100,
    "estimated_minutes_remaining": 17.3
  }
}

At a glance, an operator can see: the writer agent is actively working, the vector database is slow (but not down), and the pipeline has roughly 17 minutes of tokens left at the current burn rate. That's actionable intelligence. No digging through logs, no guessing.

Three Beginner Mistakes to Avoid

1. Blocking the Health Check with Slow Probes

Always run dependency checks with a hard timeout and always run them concurrently with asyncio.gather. A health check that takes longer than 5 seconds will be killed by most monitoring systems and load balancers, which will then incorrectly flag your pipeline as down.

2. Exposing the Health Endpoint Without Authentication

Your health check response contains detailed internal topology information. Token budget data, dependency URLs, and agent task descriptions are all sensitive. In production, protect the /health endpoint behind at minimum an API key header check, or restrict it to internal network access only.

3. Forgetting to Update Agent State on Exceptions

The most common reason agent registries become stale is that developers update state at the start of a task but forget to update it in the exception handler. Always use a try/finally block:

agent_registry.update("research-agent", AgentStatus.RUNNING, current_task="Fetching data")
try:
    result = await fetch_data()
    agent_registry.update("research-agent", AgentStatus.IDLE)
except Exception as e:
    agent_registry.update("research-agent", AgentStatus.FAILED, error=str(e))
    raise

Extending the Health Check as You Grow

Once you have the basics working, here are the natural next steps for a more production-ready implementation:

  • Add a /health/live vs. /health/ready split: Liveness checks confirm the process is running. Readiness checks confirm the pipeline is ready to accept new work. Kubernetes and most container orchestrators expect this distinction.
  • Persist health snapshots: Write each health check response to a time-series store (like InfluxDB or Prometheus) so you can visualize trends over time, not just the current moment.
  • Add per-agent token accounting: Track how many tokens each individual agent has consumed so you can identify which agent is the biggest budget consumer.
  • Integrate with alerting: Wire the rollup status to PagerDuty, Slack, or your preferred alerting tool so that an "unhealthy" response triggers an immediate notification.
  • Add a circuit breaker status: If you're using circuit breakers to protect against failing dependencies, expose the circuit state (closed, open, half-open) in the dependency section.

Conclusion

Building a multi-agent pipeline health check endpoint is one of those investments that feels optional until the moment it isn't. The difference between a pipeline that fails silently at 2 AM and burns your entire monthly token budget and one that sends you a "degraded: 17 minutes of tokens remaining" alert is exactly the 90 minutes it takes to implement what you've just read.

The pattern is straightforward: a central agent registry, a thread-safe token budget tracker, parallel dependency probes, and a rollup status computed from all three. Wire them into a single /health endpoint, and you've gone from zero observability to a real-time operational dashboard in a single API call.

Start simple. Get the basic version running today. Then layer in persistence, per-agent token accounting, and alerting integrations as your pipeline grows. The foundation you've built here will scale with you every step of the way.

Read more

7 Ways Enterprise Backend Teams Must Redesign AI Agent Graceful Degradation Strategies as Inference Provider Consolidation Reduces Multi-Vendor Fallback Options in H2 2026

7 Ways Enterprise Backend Teams Must Redesign AI Agent Graceful Degradation Strategies as Inference Provider Consolidation Reduces Multi-Vendor Fallback Options in H2 2026

For the past two years, enterprise backend teams enjoyed a comfortable safety net: if one inference provider went down or degraded, you simply rerouted traffic to another. OpenAI, Anthropic, Google Gemini, Mistral, Cohere, and a growing roster of specialized providers gave platform engineers the luxury of multi-vendor fallback trees. That

By Scott Miller
Synchronous RPC vs. Asynchronous Message Queue Orchestration for AI Agent Tool Calls: The Enterprise Backend Decision That Determines Whether Your Multi-Step Workflows Survive Partial Inference Provider Outages in H2 2026

Synchronous RPC vs. Asynchronous Message Queue Orchestration for AI Agent Tool Calls: The Enterprise Backend Decision That Determines Whether Your Multi-Step Workflows Survive Partial Inference Provider Outages in H2 2026

It started as a three-minute outage. One inference provider's GPU cluster in us-east-1 began throttling requests at 2:47 AM, and by 3:00 AM, fourteen enterprise AI workflows had silently failed mid-execution. No retries. No compensating transactions. No audit trail of which tool calls had already succeeded.

By Scott Miller