FreeAPI.watch

How I Built a Live Monitor for 77 Free APIs (Build-in-Public)

 ·  meta

Astro static + Cloudflare Workers + D1 + GitHub Actions. Total cost: $0/month. Here's the architecture and the bugs I hit.

FreeAPI.watch runs on Cloudflare's free tier and costs $0/month to operate. The architecture is a static Astro site deployed to Cloudflare Pages, with a Cloudflare Worker doing the hourly pings and writing results to a D1 (SQLite) database. GitHub Actions handles the daily site rebuild.

I built this because I kept running into the same problem: blog posts listing 'free APIs' from 2022 or 2023 with no indication that half the APIs were dead. The solution was to actually check them.

Architecture: The Three-Tier Stack

The stack has three components that interact through shared data:

Tier 1 — The pinger Worker: A Cloudflare Worker scheduled via `crons` in wrangler.toml fires every hour. It queries D1 for all active API health URLs, fires HTTP requests (GET or HEAD per API config), and writes results back. On status change, it inserts an event record.

Tier 2 — D1 database: Stores the API seed data, latest check results, 30-day rollup data (pre-computed hourly uptime percentages), and the events table. D1 is Cloudflare's serverless SQLite — free tier includes 5GB storage and 25M row reads/day, more than enough.

Tier 3 — Astro static site: Built once per day by GitHub Actions, the Astro site fetches a JSON snapshot from the Worker (`/snapshot.json` endpoint), renders all pages statically, and deploys to Cloudflare Pages. The site itself has no server-side logic — it's pure HTML.

The Bugs That Hurt

The Workers 50-subrequest limit was the first major wall. Cloudflare Workers on the free tier are limited to 50 outbound subrequests per invocation. With 77 APIs to ping, a naive implementation hits the limit every time. The fix: batch the pings across multiple invocations. The cron fires once per hour; rather than pinging all 77 APIs at once, I split them across multiple invocation slots using modulo arithmetic on the current minute.

The `Illegal invocation` error was the most confusing. When you call `fetch()` inside a Cloudflare Worker and pass a `Request` object constructed outside the current module scope, you can get an `Illegal invocation` error that looks completely unrelated to what you're doing. The fix was constructing the Request object fresh in each fetch call rather than caching it.

Direct-upload Pages doesn't support deploy hooks. I assumed I could trigger a Cloudflare Pages build via their API after the Worker updates the database. This only works for git-connected deployments — direct upload projects (which is what you get when you deploy from GitHub Actions using wrangler) have no programmatic trigger. The solution: GitHub Actions runs on a daily cron (`0 6 * * *`), fetches the snapshot, and builds+deploys with `wrangler pages deploy dist/`.

The JSON API

The `/api` endpoint (see /api for documentation) exposes the full snapshot as JSON. It's the same data the static build uses, available for anyone who wants to build on it: API names, slugs, categories, free tier info, latest check results, and uptime rollups.

Using this yourself: `curl https://freeapi.watch/snapshot.json` returns the full dataset. It's updated hourly by the Worker. The schema is documented at /api.

Cost: $0

Cloudflare Workers free tier: 100,000 requests/day, which covers 77 hourly pings (= 77×24 = 1,848/day) with room to spare. D1 free tier: 5GB storage, 25M row reads/day, 50K row writes/day — the database is a few MB and the writes are 77/hour = 1,848/day. Cloudflare Pages: unlimited bandwidth on the free tier. GitHub Actions: 2,000 free minutes/month, the daily build takes about 3 minutes.

The only potential cost is if traffic scales dramatically and Cloudflare starts charging for bandwidth — but static HTML compresses well and the free tier is generous.

Frequently Asked Questions

Can I see the source code?

The project is currently closed-source. I'm planning to open-source the monitoring worker and the seed data file — the business logic that would remain proprietary is primarily the scoring algorithm and the deployment tooling. Watch /changelog for updates.

Why Astro instead of Next.js?

Static output and zero client-side JavaScript by default. The site is a read-heavy content directory — there's no user state, no authentication, no real-time updates that need client-side hydration. Astro builds to pure HTML/CSS, which means faster page loads, better Core Web Vitals, and simpler Cloudflare Pages deployment. Next.js would work fine but it's more infrastructure than the project needs.

How does the pinger actually work?

A Cloudflare Worker runs on a cron trigger (every hour). It reads the list of APIs from D1, fires a GET/HEAD request to each health URL, records the status code and response time, and writes the result back to D1. If the status changes (alive → dead or dead → alive), it inserts an event row. The Worker runs within Cloudflare's free tier limits — the 50-subrequest limit per request was a real constraint I had to work around.

← All articles