Why I Gave Up Migrating My Blog to Cloudflare

Why I Gave Up on Cloudflare EmDash: D1 free-tier limits and an open-ended bill
この記事は約26分で読めます。
スポンサーリンク

Over the past few days, I tried to migrate my WordPress blog to EmDash, a CMS that runs on Cloudflare. I ended up back on WordPress — and I rolled back twice. The reason is simple: I exceeded the free tier’s daily database read limit.

This post walks through what I was trying to do and where it fell apart, with the terminology explained along the way.

One thing up front: I’d like to push back on the assumption that moving from WordPress to EmDash is always the right call. EmDash is genuinely good software, but it isn’t a universal fit. And signing up as an individual for a service with no spending cap is far riskier than most people realize.

What I was trying to do

First, the cast of characters.

TermWhat it is
WordPressThe most widely used blogging software in the world. Written in PHP. I’d been running it for years
AstroA framework for building fast websites. Strong at serving static pages
EmDashA CMS released by Cloudflare in 2026. Built on Astro, positioned as a successor to WordPress
CloudflareA major CDN and serverless platform. Points of presence worldwide, cheap and fast
WorkersCloudflare’s compute platform. Run code without renting a server
D1Cloudflare’s database. The free tier has a daily read limit
R2Cloudflare’s object storage, used for images
Edge cacheKeeps generated pages at each point of presence, so repeat requests never touch the database

The plan was to move off WordPress and onto EmDash running on Cloudflare. That meant migrating 1,185 posts and 8,753 images, then rebuilding the theme from scratch.

The upside looked compelling:

  • Start on the free tier (WordPress costs money every month)
  • Fast from anywhere in the world
  • No server maintenance

The verdict: the free tier wasn’t enough

Two reasons.

  • I exceeded D1’s daily read limit (5 million rows). The measured value was 10,167,276 rows — 203% of the limit
  • The paid plan has no cap. It has alerts, but nothing that stops usage, which means an open-ended bill — a scary prospect for a personal blog

My blog gets roughly 800 unique visitors and 7,000 page views per day. Even at that scale, the free tier didn’t hold.

What the free tier actually gives you

Here are the numbers from Cloudflare’s official documentation.

ResourceFreePaid
Workers requests100,000/dayUnlimited ($0.30 per million)
Workers CPU time10 ms per request5 minutes
D1 rows read5 million/dayFirst 25 billion/month included, then $0.001 per million
D1 rows written100,000/dayFirst 50 million/month included, then $1.00 per million
D1 database size500 MB10 GB
R2 storage10 GB$0.015/GB
R2 Class B operations10 million/month$0.36 per million

The unit that trips people up is the row. A single page view reads anywhere from tens to hundreds of rows from the database. You can have plenty of headroom in your request count (100,000/day) and still run out of rows (5 million/day) first.

What happened on my blog

These are measured values. Before the migration, I was comfortably inside the free tier.

DateD1 rows readShare of limitSituation
Oct 91,117,46022.3%No migration (normal operation)
Oct 106,138,200122.8%Over limit (migration work)
Oct 115,595,519111.9%Over limit (migration work)
Oct 125,357,072107.1%Over limit (migration work)
Oct 134,504,00090.08%Rolled back at 17:40 after deciding it wouldn’t hold
Oct 1410,167,276203.3%Switched back, exceeded the limit, rolled back at 19:44

Normal operation sits at 22%. Without the migration work, none of this would have been a problem. It was the migration, the verification, and repeated cache clearing that stacked up.

Oct 13 and Oct 14 were different situations. On Oct 13, I looked at the trend and pulled the plug before hitting the ceiling. On Oct 14, I actually blew through the limit.

Two days in a row where the day didn’t last

This is the part that matters. I rolled back twice.

AttemptTimeSituation
FirstOct 13, 17:40At the current rate, it wouldn’t survive until the next reset — so I rolled back while I still had margin
SecondOct 14, 19:44Switched over again the next day, hit the same wall, and rolled back

The key point is that I didn’t roll back because the site broke.

On Oct 13, I made the call before hitting the ceiling. Here’s why.

  • By 17:00, I had already consumed 90.08% of the day’s limit
  • The remaining margin wouldn’t have lasted another hour
  • The next reset wasn’t until 9:00 the following morning — more than 15 hours away

"Rather than wait for the site to fall over, roll back while I still have room." That was the call. And it worked: traffic dropped off sharply after the rollback at 17:40, so the site never went down.

Late afternoon in Japan is still the middle of the day — most of the traffic is ahead of you, not behind. If the limit is already in sight at that hour, the rest of the day is a write-off.

Here’s the hourly breakdown for Oct 13.

Hour (JST)D1 rows readNotes
09:00589,457Cache warming up right after the reset
11:00674,214Traffic begins
12:00450,538
13:001,668,317Heavy bot traffic
14:00376,589
15:00421,023WAF configured
16:00329,922
17:00459,814Rolled back at 17:40, before hitting the limit

A single hour — 13:00 — burned 1.67 million rows. Bots were concentrated in that window.

The decision to roll back the moment it was clear the site wouldn’t last until the next reset turned out to be the right one. Waiting for the limit to hit and the site to stop responding would have caused more disruption than rolling back with margin to spare.

The mitigations I applied — and why they weren’t enough

I applied every reasonable mitigation to stay inside the free tier.

Caching

The edge cache keeps generated pages at each point of presence and serves repeat requests without touching the database. It offers the biggest potential reduction in D1 reads, so I did the following.

  • Extended cache lifetimes (5 minutes → 1 hour → 1 day)
  • Applied caching to image delivery as well
  • Precomputed and stored reading times for post listings to cut per-request database lookups
  • Disabled unnecessary aggregation queries

Bot mitigation

Reviewing the access logs, I found a steady stream of scanning traffic.

  • Blocked suspicious paths (`.env`, and so on) with WAF rules
  • Enabled Bot Fight Mode
  • Rate-limited requests from regions with concentrated traffic

It still didn’t fit

The bottom line: even with all of these in place, D1 reads did not fit inside the free tier.

Two main reasons.

  • Reads from legitimate users — every page view touches the database, and it adds up even at 800 users and 7,000 page views a day
  • Persistent bot traffic — concentrated in specific windows, pushing reads higher

The mitigations helped, but reads accumulated faster than I’d anticipated.

> I never fully pinned down the cause. > Some hours the cache was clearly working; others it wasn’t. Some hours blocking bots made no difference to consumption. > I ran out of runway before I could explain why the numbers wouldn’t come down.

⚠ Clearing the cache carries risk

This is the one thing I think everyone needs to understand before running this setup.

On Cloudflare, the cache is cleared by:

  • Changing cache settings
  • Redeploying
  • Running a cache purge

These aren’t optional. You can’t avoid them — not when changing settings, not when fixing code.

But the moment the cache is cleared, every request lands on the database at once.

19:00  →  1,620,396 rows
19:15  →  3,581,449 rows
19:30  →  1,133,884 rows

> Note: Clearing the cache is something you have to do sometimes. Just be aware that D1 reads spike immediately afterward. > It’s especially easy to clear the cache several times in quick succession during something like "change a setting, then check the result."

Everything behaves correctly. You get a success response. But the side effect is a sharp jump in D1 load — and that side effect has to be part of your operational design.

The outcome

Apply mitigations → a settings change clears the cache → every request hits the database at once → the limit is reached → roll back.

I couldn’t break that cycle, and in the end I moved back to WordPress.

Why I didn’t just pay for a plan (the part I most want to get across)

The obvious question: why not just pay the $5/month? The paid plan has no limit.

But having no limit was exactly the problem.

FreePaid
Cap5 million rows/dayNone
When exceededQueries fail (stops safely)Metered billing continues
AlertsYesYes
Hard stopYes (the cap)None

The paid plan isn’t designed to "stop when you use too much." It’s designed to "bill more when you use too much."

It isn’t a " plan"

"Workers Paid $5" reads like $5 a month gets you the service. In practice, $5 is a minimum charge, and everything above it is metered.

And alerts don’t stop anything. Cloudflare’s own documentation states plainly that budget alerts do not pause or cap usage.

Other people’s experiences

When I went looking, I found similar stories reported elsewhere.

CaseWhat happened
A solo developerA bug wrote a huge number of rows to D1; the bill came to $4,868 against a usual $5/month
A Durable Objects incidentA health check looped; the bill was reported at $34,895

These are public reports, not something I verified myself. But they share a theme: by the time you notice, it’s too late.

Alerts arrive by email, so they don’t help while you’re asleep or away from your desk. Waking up to a bill that’s orders of magnitude larger is a real possibility.

"It won’t happen to me" isn’t something I was willing to bet on — and that’s ultimately what drove the decision.

AI and bots make this worse

It used to be reasonable to assume human traffic had a natural ceiling. Not anymore.

  • AI crawlers sweep thousands of pages at a time
  • Automated bot requests can multiply your request count in minutes
  • A post going viral can bring tens of thousands of visits at once
  • A loop in your own code can write indefinitely

On the free tier, the worst case is "you hit the limit and everything stops." On the paid plan there is no stop — the meter just keeps running.

For a personal blog, that’s a risk I wasn’t willing to take.

Cloudflare is working on a cap

In fairness, Cloudflare recognizes the problem. Their blog (updated in 2026) says:

> We’re prototyping hard spending caps now, with early availability in Q4 2026.

They’ve also already shipped a billable usage dashboard, and budget alerts are on by default. The direction is right, but there’s still no hard stop.

I’ve paused the migration for now, but if that spending cap ships, I’d revisit the idea.

EmDash isn’t a silver bullet

Reading this, you might conclude that EmDash is the problem. It isn’t — it’s good software.

  • Built on Astro, so it’s fast
  • A modern, pleasant admin UI
  • Sandboxed plugins (an attempt to fix WordPress’s plugin security model)
  • Officially supports migration from WordPress

The problem wasn’t EmDash. It was that the free tier’s constraints didn’t match the size of my site.

Three assumptions that are easy to miss

These tend to get lost when evaluating a migration.

Common assumptionReality
"It’s a static site, so it doesn’t use the database"The admin UI, listings, and aggregations all read the database — on every page view
"Plenty of headroom in the request count"The limit isn’t on requests, it’s on rows. One request can read hundreds
"Just configure caching and you’re set"Every cache-clearing operation (settings change, deploy, purge) sends load straight to the database

The third is the one to watch. A settings change made to reduce load can, in the moment, increase it instead.

Migration isn’t a solution, it’s a trade

WordPress has real problems.

  • Monthly hosting costs
  • Plugin security risk
  • Slow rendering
  • Ongoing maintenance

EmDash solves those. But it comes with a different set of constraints.

Again, this isn’t really about EmDash — it’s about the Cloudflare + EmDash architecture as a whole. EmDash itself is a solid piece of software. The constraints below come from the platform it’s running on, not from the CMS.

  • The free tier has a hard limit
  • The paid plan has no limit (which is the problem)
  • You need a solid understanding of caching behavior

It isn’t "moving makes things better" — it’s "moving trades one set of problems for another." That’s the biggest lesson from this whole exercise.

Conclusion: if you have more than 500 daily uniques, don’t migrate to the free tier

Working backward from the measurements:

  • On my blog (800 users / 7,000 page views), normal operation is 22% and migration work pushed it to 100–200%
  • On Oct 13 I rolled back at 17:40 before hitting the ceiling; on Oct 14 I rolled back at 19:44 after actually exceeding it
  • Both times I had to concede the rest of the day, well before the day was over
  • In other words: the moment you do anything that clears the cache — a migration, a rebuild — you blow past the limit
  • Above roughly 500 users, the simultaneous requests after a cache clear translate directly into database load

If your blog has more than 500 daily unique visitors, I’d avoid migrating to the free tier.

And "it just has to last until the evening" doesn’t save you. If the limit comes into view in the late afternoon, the rest of the day’s traffic has nowhere to go.

That said, it’s worth considering if any of these apply.

  • A few hundred uniques or fewer, with infrequent updates
  • The paid plan ($5/month minimum), with alerts and budget monitoring that you actively manage yourself
  • A design that barely reads the database (fully static generation, for example)

Summary

  • I tried to move from WordPress to EmDash (Cloudflare) and gave up when I exceeded the D1 read limit
  • Two rollbacks in two days (Oct 13 at 17:40, Oct 14 at 19:44) — both times I had to decide before the day was out
  • On Oct 13 in particular, I called it before hitting the ceiling and rolled back with margin to spare
  • The peak was 10,167,276 rows — 203% of the 5 million rows/day free-tier limit
  • Extending caching and blocking bots did not stop reads from climbing
  • Not fully identifying the cause was the biggest gap
  • Cache-clearing operations (settings changes, redeploys, purges) send every request to the database at once
  • EmDash itself is good software. What didn’t fit was the free tier’s constraints versus my site’s size
  • Migration is a trade-off, not a cure. Caching behavior in particular has to be understood up front
  • The paid plan has no cap and no hard stop. $5 is a minimum, and you’re accepting unlimited downside
  • In the age of AI and bots, traffic spikes are a question of when. Others have reported the same pattern
  • If you have more than 500 daily unique visitors, don’t migrate to the free tier

One last thought

Lately I keep seeing the assumption that migrating from WordPress to EmDash is simply the right move.

The destination is genuinely appealing. But it isn’t a silver bullet. And the danger of an individual signing up for an uncapped service deserves far more attention than it gets.

I’ve shelved the migration for now, but if Cloudflare ships hard spending caps, I’d like to revisit it (their blog points to early availability in Q4 2026).

I’ll write a separate, more detailed post on exactly what I tried and where it broke down.

コメント

タイトルとURLをコピーしました