Engineering

How We Built a Real-Time API Uptime Monitor for 200+ Endpoints

An engineering breakdown of the concurrency model, batching strategy, and circuit-breaker patterns behind DeveloperLab's live health checker — probing 200+ third-party endpoints every 20 minutes without hanging.

When we set out to add live health data to DeveloperLab's API directory, the goal was simple: every API card should show a real HTTP status code and a server-measured latency — not a stale badge copied from a vendor's status page. What we didn't expect was how quickly "simple" became "concurrency problem."

The Naive Approach (And Why It Failed)

The first prototype made sequential fetch calls. One endpoint after another, in a tight loop. With 199 APIs in our catalogue, a single slow endpoint (looking at you, some government data portals) would stall the entire probe run for up to 30 seconds — blocking the event loop and making the server feel sluggish during the sweep.

Serial probe for 199 endpoints × avg 400 ms = ~80 seconds per run

That's completely unacceptable for a tool that's supposed to provide live data.

Batched Concurrent Probing

The architecture we settled on runs probes in batches of 25 concurrent requests, each with a hard 5-second timeout. Node.js's non-blocking I/O handles concurrency elegantly here — 25 in-flight fetch calls don't spin up 25 threads; they issue 25 network requests and yield control back to the event loop.

const BATCH_SIZE = 25;
const TIMEOUT_MS = 5000;

async function probeBatch(entries: ApiEntry[]): Promise<void> {
  await Promise.allSettled(
    entries.map(async (entry) => {
      const start = Date.now();
      try {
        const res = await fetch(entry.Link, {
          signal: AbortSignal.timeout(TIMEOUT_MS),
          redirect: "follow",
        });
        entry.last_checked_status = res.status;
        entry.latency_ms = Date.now() - start;
        entry.response_headers = {
          server:           res.headers.get("server")           ?? undefined,
          "content-type":  res.headers.get("content-type")     ?? undefined,
          "cache-control": res.headers.get("cache-control")    ?? undefined,
        };
      } catch {
        entry.last_checked_status = null;
        entry.latency_ms = null;
      }
    })
  );
}

Promise.allSettled is critical here. Unlike Promise.all, it never short-circuits on a rejected promise. One timed-out endpoint doesn't kill the rest of the batch.

Slicing Into Batches

async function startApiHealthChecker(): Promise<void> {
  const entries = loadStoredData().entries;
  for (let i = 0; i < entries.length; i += BATCH_SIZE) {
    await probeBatch(entries.slice(i, i + BATCH_SIZE));
  }
  saveStoredData({ entries, lastUpdated: new Date().toISOString() });
}

With 199 entries, that's 8 batches. Worst case (all 25 endpoints in a batch hit the 5-second timeout), a single batch takes 5 seconds. Total worst-case sweep: 40 seconds. Typical real-world sweep: under 30 seconds.

The Numbers After One Week

Metric Value
Total endpoints probed 199
Average sweep duration 23 seconds
Consistently reachable 192/199 (96.5%)
7 unreachable Mostly government/academic APIs with Cloudflare blocks
Probe interval 20 minutes

The 7 endpoints that show as unreachable aren't down — they're blocking our probe's User-Agent or requiring cookies. We mark those with a null status rather than reporting them as errors to avoid misleading users.

Persisting Health Data Without a Database

We write health fields (latency_ms, last_checked_status, response_headers, last_health_check) directly back into public_apis.json after every sweep. No PostgreSQL. No Redis. No cloud infrastructure.

This means:

The tradeoff: health data is lost on server restart. In practice, the first probe completes 30 seconds after startup, which is acceptable.

What's Next

We're exploring delta updates — only re-probing endpoints whose status changed in the previous sweep, plus a random 20% sample to catch intermittent failures. This would cut sweep time to under 5 seconds for most runs.


The source for the health checker lives in src/lib/apiHealthChecker.ts. If you have questions about the implementation or spot an API that's incorrectly flagged, open an issue or send feedback via the footer.