Skip to content

Instantly share code, notes, and snippets.

@zeke
Created July 22, 2026 03:50
Show Gist options
  • Select an option

  • Save zeke/72c6a90414ca0067e7fb446d795337ff to your computer and use it in GitHub Desktop.

Select an option

Save zeke/72c6a90414ca0067e7fb446d795337ff to your computer and use it in GitHub Desktop.
When Cloudflare Blocks Cloudflare: A Browser Run Case Study

When Cloudflare Blocks Cloudflare: A Browser Run Case Study

Context

yogo is a small project that aggregates public yoga/fitness class schedules from studio websites and booking platforms, so practitioners can search across studios in one place instead of checking a dozen separate booking systems. It runs on Cloudflare Workers, D1, Queues, and Browser Rendering (Browser Run), and uses an LLM+browser agent to discover each studio's schedule once, then (where possible) replays a deterministic, browser-free HTTP request on a schedule ("recipes") instead of re-running the agent every time.

This report is about one studio — Yoga Soup in Santa Barbara, CA, which uses Union.fit — and what happened trying to read its public class schedule. It's shared for two reasons: it's a clean example of Browser Run itself being blocked by another Cloudflare zone's bot management, and it's an example of a booking platform's bot policy having a real, if probably unintended, side effect on the practitioners the platform ultimately serves.

This report is intended to start a conversation between the Browser Run team, the Union.fit team, and anyone else who runs into the same pattern.

What was found

Yoga Soup's schedule lives on yogasoup.union.site, a Union.fit-hosted subdomain (Union.fit also serves the same org at union.fit/orgs/yoga-soup). Both are Cloudflare-fronted and return a Cloudflare interstitial challenge ("Just a moment... Performing security verification") to any request that isn't a real, JS-executing, non-automated browser.

Three things were confirmed directly, live, in this order:

  1. A plain fetch() / curl gets 403 with the challenge page. Expected — this is how most bot-protected sites behave against server-to-server requests.
  2. Cloudflare's own Browser Rendering (Browser Run) also gets the challenge, and never resolves it. Tested against the same URL with a real Browser Run CDP session, including the beta lab=true Chrome pool. It sits on "Just a moment..." indefinitely.
  3. A real, local, non-headless, non-automated Chrome instance (manual --remote-debugging-port, no automation flags) loads the page normally and returns the full real schedule. navigator.webdriver is false on this session; it's a genuinely regular browser, just remotely inspectable.

The reason for (2) turned out to be explicit, not incidental. yogasoup.union.site/robots.txt ships a block, under a header literally reading # BEGIN Cloudflare Managed content, that includes:

User-agent: CloudflareBrowserRenderingCrawler
Disallow: /

alongside disallow entries for GPTBot, ClaudeBot, CCBot, Bytespider, Amazonbot, Google-Extended, and meta-externalagent. This is Cloudflare's own managed-robots.txt bot-exclusion list, which ships a named signature specifically for Browser Rendering so a zone owner can opt out of it by name — and Union.fit's zone (or Union.fit itself, on behalf of every org it hosts) has opted out. The root path also returns cf-mitigated: challenge, and this challenge is zone-wide, not scoped to one page: even a clean, well-shaped internal JSON endpoint discovered during this investigation (/orgs/<org>/fullcalendar.json, real FullCalendar-format event data used by Union's own Calendar page template) returns the identical 403 challenge to anything but an already-challenge-cleared browser session.

Checking Union's public developer docs (docs.union.fit, not challenge-gated) confirmed there's no public third-party read API either — their only two integration surfaces are a Customer.io CRM sync (outbound member/event data for marketing, not schedule data) and a Liquid templating system for a studio's own "Branded Site," which runs authenticated, server-side, inside Union's own infrastructure.

Net effect: there is currently no way, from Cloudflare Workers using Cloudflare's own Browser Rendering product, to read a Union.fit-hosted studio's public class schedule. Not because the data is private (it's the exact same information a prospective student sees on the page, unauthenticated), but because Union.fit's Cloudflare configuration specifically excludes Cloudflare's own browser automation product by name.

For comparison: platforms this project can read cleanly

This isn't a general complaint about bot protection — most booking platforms this project touches are considerably more permissive, and several patterns already work well, ordered here from most to least widely used across studios in this project's dataset:

Platform Access method Any bot friction?
Mindbody Browser Run session running fetch() in-tab (see note below) Blocks direct Worker fetch() egress only
Momence Plain unauthenticated fetch() to a JSON API None
Mariana Tek Plain unauthenticated fetch() to a JSON API None
CorePower Yoga Plain unauthenticated fetch() (POST, Elasticsearch DSL) to a JSON API None
Arketa Plain unauthenticated fetch() to a JSON API None
Google Calendar (public iCal export) Plain unauthenticated fetch() of a public iCalendar (.ics) feed None
Daxko (YMCA program finder) Plain unauthenticated fetch() to a JSON API, one request per day None
Independent studio websites Plain unauthenticated fetch() of static/server-rendered HTML None

The Mindbody/Healcode row is worth calling out specifically, because it's a useful contrast with Union.fit's approach rather than just another "it works fine" row. Healcode's widget endpoint also returns 403 to a plain Worker fetch() — even with a realistic browser User-Agent and Referer header — but returns 200 with real data to the identical request run from inside a Browser Run session's own in-tab fetch(). That looks like a WAF rule scoped to Workers' server-side fetch egress specifically (likely IP/ASN-based), not a rule against automated browsing generally: a real, if headless and scripted, Browser Run session is treated as legitimate browser traffic and let through. That's a materially different design choice than blocking browser automation by product signature at the root, and it still gets the job done at near-zero cost (~$0.00007 per replay, confirmed live, vs. $0.12–$0.59 for the same studios under a full LLM+browser discovery run).

Four Mindbody-hosted studios' classic scheduling widget (a different, older Mindbody surface than Healcode) were separately reported by this project's discovery agent as hitting a similar Cloudflare "security verification" interstitial. That wasn't investigated to the same depth as Union.fit in this report (no direct Browser Run/robots.txt test against that specific surface), so it's included here as a secondary data point, not an equally-confirmed one.

Why this matters beyond one integration

The practical effect of Union.fit's current configuration is that a studio using Union.fit is invisible to any aggregator trying to give practitioners a single place to compare class times and availability across studios — not because the studio or Union.fit intends to hide that information (it's the same page a prospective student would look at directly), but because the specific mechanism used to keep out AI training/scraping bots also, by construction, keeps out anything trying to make that same public information more discoverable to the people it's meant for. Aggregation isn't the only case this affects: any Cloudflare Workers project trying to build a legitimate integration against a Union.fit-hosted site via Browser Rendering runs into the identical wall.

This report exists to open a conversation, not to argue for a specific fix. A few directions seem worth putting on the table:

  • For Union.fit: a documented partner/read API (even a limited, schedule-only one) would remove the need for anyone to touch browser automation against their sites at all — the actual ask underneath this whole investigation.
  • For Browser Run / Cloudflare Bot Management: it might be worth knowing that the managed-robots.txt "block CloudflareBrowserRenderingCrawler" default is being used, in at least this one real case, to block Cloudflare's own product wholesale rather than to filter out clearly abusive AI-training crawlers specifically — worth knowing whether that's the intended scope of that list entry.
  • For anyone hitting the same wall: the Mindbody/Healcode pattern above (an in-tab fetch() from an otherwise-normal Browser Run session, rather than a top-level page navigation) is a real, working way to get real data from at least one similarly bot-protected platform, cheaply. It didn't work here because Union.fit's block operates at a different layer (an explicit product-signature exclusion, not a Worker-egress-IP rule), but it's a useful thing to try first against a new target before assuming a whole avenue is closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment