Zero Isn’t Zero, Part 2: Keeping Strangers From Waking My Apps

Published in Laravel, We Must Ship, Laravel Cloud, Zero Isn’t Zero on Nov 4, 2026

Zero Isn’t Zero, Part 2: Keeping Strangers From Waking My Apps

In part one I ran three side projects on Laravel Cloud for a month and paid $4.87. The biggest surprise wasn’t the cron jobs or the queues. It was who kept waking the apps up.

All three apps sit behind Cloudflare, and 31 days of its analytics told a clear story:

MastersLog Boating GoodHabits
Requests that were machines 91% 79% 92%
Probes (dotfiles, config paths, 404s) 64% 64% 73%
Verified bots (search, SEO, AI crawlers) 28% 15% 20%
Served from Cloudflare’s cache 0.6% 0% 0.1%

Nine in ten requests came from machines, and almost none were answered at the edge. On a scale-to-zero platform, every request that reaches the app wakes it, and you pay for the full sleep timeout after each wake. Replaying the month without the machine traffic cut awake time roughly in half for every app.

The fix is simple once you see it: anything Cloudflare answers or blocks at its edge never reaches Laravel Cloud, so it can’t wake anything. Here is what I set up, all on Cloudflare’s free plan.

1. Block probes

Most of the traffic was scanners asking for /.env, /.git/config, /.ssh/id_rsa, /wp-login.php and a long tail of random .php files. A Laravel app sends everything through index.php, so no real URL in it ends in .php or starts with a dot. That makes this custom WAF rule safe:

(starts_with(http.request.uri.path, "/.") and not starts_with(http.request.uri.path, "/.well-known/"))
or ends_with(http.request.uri.path, ".php")
or http.request.uri.path contains "/wp-"
or http.request.uri.path contains "/vendor/"
or http.request.uri.path contains "phpmyadmin"
or ends_with(http.request.uri.path, ".sql")
or ends_with(http.request.uri.path, ".bak")

Action: Block. One honest caveat: a scanner firing 300 requests in seven seconds only costs one wake, so blocking probes saves less awake time than their share of requests suggests. It’s still a two-minute rule, and it stops them from ever reaching PHP, which felt especially good the week PHP shipped two critical RCE fixes.

2. Block scripted clients

One app took almost 6,800 curl requests in a month, all in scanner bursts. A second rule blocks the usual scripting tools and empty user agents, while letting Cloudflare-verified bots through:

(http.user_agent contains "curl" or http.user_agent contains "python-requests"
 or http.user_agent contains "Go-http-client" or http.user_agent contains "Wget"
 or http.user_agent contains "zgrab" or http.user_agent contains "libwww-perl"
 or http.user_agent eq "")
and not cf.client.bot

Before turning this on, I checked a week of successful POSTs and non-browser traffic for anything legitimate: webhooks, API clients, my own scripts. The only POSTs that got a normal response were Livewire updates. If your app receives webhooks, check their user agents first.

3. Decide what crawlers actually need

My apps are logged-in tools. Search engines only need the landing page:

  • MastersLog and GoodHabits: verified crawlers can reach / and /robots.txt; everything else is blocked.
  • Boating: a private reservations app, so crawlers are blocked entirely.
cf.client.bot and not http.request.uri.path in {"/" "/robots.txt"}

On top of that I turned on two switches under Security → Bots: Block AI bots and Bot Fight Mode, which goes after the “fake browser” traffic that hides among the possibly-human requests. Two caveats: on the free plan Bot Fight Mode can’t be bypassed by your own rules, so it can break a webhook or API client, and it needs Cloudflare’s JavaScript detections, which inject a small script into your pages.

4. Cache what crawlers ask for on its own

With under 1% served from cache, a crawler asking for nothing but robots.txt or favicon.ico woke the whole app. One cache rule fixes that:

http.request.uri.path in {"/robots.txt" "/favicon.ico" "/sitemap.xml"}
or starts_with(http.request.uri.path, "/build/")

It’s eligible for cache with a one-day edge TTL. robots.txt now comes back from Cloudflare on every repeat request. HTML pages still don’t cache, because Laravel sets a session cookie on every response. Fixing that means serving public pages without the session middleware, which is a job for another day.

5. Let the edge do the redirect

Two of my zones had Always Use HTTPS turned off, so every http:// request travelled all the way to Laravel Cloud just to be redirected. Now Cloudflare sends the redirect itself.

What I skipped

Rate limiting. The free plan gets one rate-limit rule, and it stops bursts. Bursts already cost only one wake each. The steady drip of a crawler every few minutes is what costs money, and the rules above go after that directly.

Did it work?

The quick checks passed right away: every probe I tried (/.env, /info.php, /wp-login.php) gets a 403 from Cloudflare, robots.txt is a cache hit, and the apps load normally. The number that matters arrived this week with the October invoice.

September October
Bill $4.87 $3.84
App compute $2.78 $1.86
MastersLog awake 187 h 118 h
GoodHabits awake 95 h 71 h
Boating awake 35 h 24 h

October has one more day than September, and through the 18th it was tracking September almost to the hour. Left alone, it would have come in at about $5.03. Instead, awake time fell by a third and the bill by about $1.20. About 16 cents of that came from merging two MySQL databases into one cluster the same week, not from Cloudflare.

The monthly totals undersell it, because the new rules only covered the last ten days of the month. Over the last two weeks of October, MastersLog averaged about 1¼ awake hours a day, down from 6. GoodHabits averaged about 1¼, down from 3. That’s better than the “up to half” I expected from the replay.

What didn’t move

$1.56 of the $3.84 doesn’t care about traffic at all:

  • $0.90 of MySQL storage and backups. The 5 GB minimum is billed whether the database is awake or not.
  • $0.66 for queues. Each app logged about 134,000 queue operations in October, nearly the same for all three despite very different traffic. That’s roughly one every 20 seconds: the worker asking for work, not doing it.

That’s now 40% of the bill, and no edge rule can touch it. If an app only sends the occasional email, dropping the queue entirely (gist) is the next lever.

What’s next

My rough projection for November is about $2.50: under a dollar of app compute, plus the $1.56 that never sleeps. I’ll also pull Cloudflare’s per-rule counts to see which rule actually earned its keep.

To be continued…