Laracon 2026 follow-up · research memo

Scale to Zero on Trial

Testing the claim that Laravel Cloud beats a $6 Forge box — against four real apps, their real cron schedules, and today's published rates.

The claim holds — but not for a lift-and-shift. Moved naively, your four apps cost more on Cloud than on Forge. Architected around one mechanic — how long the stack stays awake — they cost roughly a third less. The talk isn't "is Taylor right." It's "here is the one number that decides whether he's right for you."

$18
Real Forge baseline, per month, all four apps
~$31
Cloud, lift-and-shift as-is
~$12
Cloud, architected for sleep
What's in here
  1. Your baseline is wrong — and it's not $6
  2. Two findings that delete most of your plan
  3. The only mechanic that matters: duty cycle
  4. Your actual schedules, audited
  5. The Postgres trap
  6. Database topology: the decision you flagged
  7. Blockers found in your code
  8. September 1st, honestly
  9. The demo that makes the talk
  10. Verify before you present

1. Your baseline is wrong — and it's not $6

The $6 in your notes is the droplet. It isn't what you pay. Forge is a subscription on top of the server, and the cheapest plan that provisions unlimited VPS servers is Hobby at $12/month.

Forge sideMonthlyNote
Forge Hobby subscription$12.00Flat, regardless of app count
VPS (the "$6 box")$6.00Hosts all 4 apps
True baseline$18.00$4.50 per app

This changes the whole framing, and it's the strongest opening you have. Cloud doesn't need to beat $6. It needs to beat $18. That's a far easier bar — and saying so on stage is more interesting than agreeing or disagreeing with the keynote, because it reframes a comparison most of the room is also getting wrong.

Check first

You've been on Forge since launch. Confirm what you're actually billed — legacy plans differ from today's published tiers, and a grandfathered rate would change your personal number (though not the one your audience will pay).

The trap in partial migration

Forge's $12 is flat. Moving one app off saves you nothing — you pay Forge $12 and Cloud on top. Savings are binary: they arrive only when the last app leaves. Budget for a period of paying both.

2. Two findings that delete most of your plan

You named a database migration as the major hurdle and budgeted serious time for it. Both halves of that concern turn out to be wrong.

Finding 1 — MySQL already scales to zero

As of July 20, 2026, Laravel MySQL Flex sizes scale to zero. Sleep is triggered by an idle timeout you configure between 1 minute and 1 hour; the next connection wakes it. A proxy layer holds the incoming connection while compute resumes, so you get a slower first query, not a connection error — which directly answers the "DB is asleep and it will fail" worry in your notes. Storage stays online and billed the whole time.

You do not need to migrate any engine. Cut that work item.

Finding 2 — one app is already Postgres

ocean-city/config/database.php defaults to pgsql. Your highest-traffic app isn't part of the MySQL problem you were bracing for. The premise "most of my sites run on MySQL and that's the blocker" doesn't survive contact with the code.

Between them, these remove the single largest task from your plan and the single largest risk from your demo. That frees the September window for the work that actually determines the answer — which is all in the next section.

3. The only mechanic that matters: duty cycle

Compute is billed per second while awake. So your bill isn't a function of traffic — it's a function of how many seconds per month the stack is not asleep. And one line in the docs makes that number much larger than intuition suggests:

The rule that drives everything

"When the environment wakes for a scheduled task or a manual wake interval, it stays awake for the duration of the sleep timeout."

A cron job that runs for two seconds doesn't bill two seconds. It bills the entire sleep timeout — and any inbound request resets the clock.

awake_seconds/month ≈ (wakes × sleep_timeout) + traffic_seconds cost ≈ awake_seconds × compute_rate + awake_seconds × db_compute_rate + storage (billed 24/7, even asleep)

Two consequences follow, and they're the practical heart of your talk:

Cloud doesn't run schedule:run every minute against a sleeping app. It captures php artisan schedule:list at deploy time, stores it, and computes the next due moment for each task so it can wake at exactly the right time.

Gotcha worth a slide

Because the schedule is captured at deploy time, changing a cron expression has no effect until you redeploy. This will bite someone in your audience.

4. Your actual schedules, audited

Read from routes/console.php in each repo. Your notes guessed "once maybe twice a day." That's right for two apps and badly wrong for the third.

AppScheduled tasks found Wakes/dayAwake @ 5-min timeoutVerdict
masterslog 1 × daily() — purge expired team invitations 12.5 h/mo Ideal
boat.kzlvs.com 2 daily + 2 weekly, all at 03:00 12.5 h/mo Ideal
ocean-city.rentals barefoot:unit:poll → everyFifteenMinutes(), plus daily sitemap 97240 h/mo 33% awake

Share of the month awake — from cron alone

Awake hours from real schedules at published sleep timeouts

Ideal Projection Today
masterslog
1 daily wake · 2.5 h/mo
0.3%
boat.kzlvs.com
all cron at 03:00 · 2.5 h/mo
0.3%
ocean-city.rentals now
everyFifteenMinutes · 240 h/mo
33%
ocean-city tuned
30-min poll + 1-min timeout · 24 h/mo
3.3%
0%25%50%75%100%

One app is 99% of the risk. everyFifteenMinutes() on a 5-minute timeout keeps ocean-city — and every database it touches — awake a third of the month. Three fixes, in order of preference:

  1. Drop the sleep timeout to 1 minute. 33% → 6.6%, zero code change. Do this first.
  2. Relax the poll to every 30 minutes if the Barefoot data tolerates it. Combined with the above: 3.3%.
  3. Move the poll onto a Managed Queue so it runs on its own worker that scales to zero independently, and never holds the web tier awake.

Worth saying plainly on stage: the tuned number is a projection from published rates and your real schedules, not a measurement. Landing it as a measurement is exactly what the migration is for.

5. The Postgres trap

You assumed Postgres was the privileged engine and MySQL the poor cousin. At the published rates it's the reverse, and it matters most for exactly the app with the highest duty cycle.

EngineBilling shapeIf awake 100%
MySQL Flex 512 MBPer-second, published monthly figure~$6
Serverless Postgres$0.320 per CPU-hour, no monthly figure quoted~$58 @ 0.25 CU

Postgres is priced as an uncapped hourly rate. That's cheap when the database genuinely sleeps and expensive the moment duty cycle climbs — which is precisely ocean-city's situation today. So the migration worth considering isn't MySQL → Postgres. It's ocean-city Postgres → MySQL, and only if its duty cycle stays high after tuning.

Verify this one

This is the number I'd least want you to state on stage unverified. Run both shapes through the Cloud pricing calculator at your real duty cycle before quoting it. The direction is solid; the magnitude deserves a second source.

6. Database topology: the decision you flagged

You called this "critical to get right," and guessed each app needs its own cluster. Half right — and the half that's wrong is the expensive half.

A cluster holds many databases (schemas). Four apps can share one. But scale-to-zero is a property of the cluster, not the schema — a shared cluster only sleeps when every app on it has gone idle. That gives a clean rule:

The rule

Share a cluster among apps whose idle windows overlap. Isolate any app that would hold the cluster awake for the others.

ClusterContentsWhy
Shared Flex 512 MB masterslog + boat.kzlvs + the 2 remaining Forge apps All quiet; boat and masterslog already cluster their cron near 03:00, so they wake together and sleep together
Dedicated Flex ocean-city.rentals Its polling would pin the shared cluster awake and hand the quiet apps a bill they didn't earn

The saving is structural, not marginal. MySQL storage starts at 5 GB and bills whether or not the database sleeps, so every extra cluster is a permanent floor you pay forever. Four clusters means paying that floor four times over — before a single query runs. Two clusters instead of four is the difference between the architected column and the naive one in the headline numbers.

Don't miss

MySQL defaults to 7-day backup retention, billed separately at $0.10/GB-month. Fine for production; pure waste on a throwaway demo environment. Set it to 0 for anything disposable.

7. Blockers found in your code

Fix before the demo

masterslog defines app/Console/Commands/SweepMaintenance.php (signature maintenance:sweep) — and nothing schedules it. The only scheduled task in that app is the daily invitation purge.

Your demo plan is "observe crons and queues run by checking sent timely reminders." The command that rolls maintenance schedules forward isn't wired to the scheduler, so there are no timely reminders to observe. This would have surfaced live, on stage.

Interruption risk

boat.kzlvs.com runs Vessel::each(fn($v) => UpdateVesselHours::dispatchSync($v)) inside a scheduled closure. That's an unbounded loop running synchronously in the wake window — and per the docs, the app cluster stops when the sleep timeout elapses even if work is still running. Enough vessels and the sweep gets killed mid-flight, silently.

Fix: real dispatch() onto a Managed Queue, which runs on independent workers and is never interrupted by the app sleeping.

Subtle, and worth stage time

Cloud points the scheduler's cache driver at your environment's cache store. Tasks using withoutOverlapping() or onOneServer() query that store on every run — and those lookups can keep an attached database or cache awake, defeating scale-to-zero. A defensive habit picked up from years of Forge quietly becomes a line item on Cloud. That's a great slide.

On queues generally: you guessed image uploads. The code says otherwise — ocean-city has 11 ShouldQueue classes, boat has 8, masterslog only 2. Note that boat's scheduled work uses dispatchSync, so those jobs never touch a queue today. Worth documenting properly, as your notes intended — the answer is more interesting than the guess.

Plan ceiling

Starter allows 1 managed queue per environment, max 3 workers, Flex compute up to 1 GB. Enough for all four apps as they stand — but ocean-city at "hundreds of pages" is the one that could outgrow it. Also note deploying a managed queue sets QUEUE_CONNECTION=cloud environment-wide, silently rerouting every job that didn't name a connection.

8. September 1st, honestly

That's tomorrow. "Move everything I have on Forge to Cloud" is not a one-day job — the cluster topology alone needs deliberate setup, and rushing it produces exactly the naive configuration that loses the argument.

The good news is that the deadline was mostly protecting you from the engine migration, and that work no longer exists. Suggested sequence:

  1. Before Sept 1 — masterslog only. One daily cron, two queued classes, lowest possible risk. Schedule maintenance:sweep first so the demo has something to demonstrate. Attach object storage for images, per your note.
  2. Week 1 — boat.kzlvs.com. Its critical cron is the thing worth proving. Convert the dispatchSync loop to a Managed Queue on the way in.
  3. Week 2 — ocean-city.rentals. The interesting one. Tune the poll interval and sleep timeout before cutover, not after, and let it run long enough to produce a real bill.
  4. Only then — the last two apps, and cancel Forge. Until that step, you're paying both platforms and saving nothing.
This is the better talk anyway

"I moved one app, measured it, and here's what the bill actually did" beats "I moved everything in a weekend and here are my projections." You'll have real invoice data for one app instead of estimates for four — and the migration-in-progress framing lets you show the naive configuration and the tuned one side by side. That comparison is the talk.

9. The demo that makes the talk

Your instinct to generate fake maintenance reminders is right, but aim it at a sharper target. Don't demo "the app works on Cloud." Demo the duty-cycle cliff — the thing nobody in the room has internalized.

  1. Seed fake maintenance and reminder data in masterslog so maintenance:sweep has real work and reminders actually send.
  2. Run one week with a 5-minute sleep timeout and a 5-minute task. The app never sleeps. Screenshot the hourly usage breakdown on the Usage page.
  3. Change only the timeout to 1 minute. Redeploy. Same app, same traffic, same schedule. Screenshot again.
  4. Put the two usage graphs side by side. One config knob, most of the bill. That's your closing slide, and it's a finding rather than an opinion.

The Usage page gives an hour-by-hour breakdown by app and by resource, so this is directly screenshottable — no instrumentation needed. Set a hard org spend limit before you start, so a misconfigured demo can't produce a surprise invoice mid-experiment.

10. Verify before you present

Everything above is drawn from published documentation and your repositories as they stand today. Five things I could not confirm from here, in rough order of how badly a wrong answer would hurt on stage:

Your thesis, in one line

Scale to Zero is cheaper than a Forge box — but it moves the cost from a monthly invoice you ignore to a cron expression you wrote two years ago and forgot. The bill is now a function of your code. That's the story, and it's better than a verdict.

Sources

Research memo prepared August 31, 2026. Cost figures are projections from published rates applied to your real schedules — not measurements. The migration is what turns them into measurements.