I gave my side project a sleep schedule on Krova Cloud to save money — power off at 1 AM, wake at 7. The surprise side effect: zero attack surface while asleep. Here’s the setup, the cron, and the trade-offs.
At 3 a.m., my production server is off. Not down — off. On purpose.
It started as a money thing. The project has human hours: traffic ~8:00 to midnight, dead until morning. The old VPS ran 24/7 and I paid for eight idle hours every night. Also, 3 a.m. on a VPS is when the botnet zoo shows up — brute-force loops, scanner sweeps, auth-log horror anthologies.
Then it clicked: this project has a sleep schedule. Servers are allowed to have sleep schedules.
The setup
The project now lives on a Cube on Krova Cloud — a Firecracker microVM with its own kernel and no public IP (private NAT’d network, managed TLS ingress, only explicitly opened ports reachable). The sleep schedule is the dumbest automation I own:
# crontab -e — on a host OTHER than web-1 (CI cron / small always-on box)
0 1 * * * krova cubes power-off web-1 # lights out: compute billing stops, disk preserved
0 7 * * * krova cubes wake web-1 && sleep 15 && curl -sf https://app.example.com/healthz || curl -sS -X POST "$ALERT_URL" -d 'text=web-1 failed to wake'
Enter fullscreen mode Exit fullscreen mode
Those are real CLI verbs (@krovacloud/cli), not pseudocode. Semantics per the docs:
- running → billed per minute for compute + disk
- stopped → billed for disk storage only
- data survives both directions; wake starts the Cube from its saved disk
So sleeping 8 hours a day removes ~a third of compute time from the bill, and the stopped hours cost pocket change. On my 2 vCPU / 4 GB / 40 GB Cube that’s the difference between “hosting” and “rounding error.” (krova pricing prints the per-resource hourly rates if you want to do your own math.)
The surprise: the 3 a.m. noise died
First week I checked the logs out of habit. Nothing. No brute force, no scanners. Of course:
- no SSH daemon listening
- no web server process
- nothing in memory, no kernel of mine executing
- nothing to scan, because there’s no public address anyway
You cannot exploit a process that isn’t running. The nighttime attack surface isn’t small; it’s zero. Awake, the Cube still isn’t a scannable box — no public IP, default-deny inbound — so the daytime surface stays tiny too.
Verify the states yourself:
krova get web-1 --json | jq -r .state # "stopped" at 1:05 AM
krova cubes wake web-1
krova ssh web-1 -- uptime # back in seconds, disk intact
Enter fullscreen mode Exit fullscreen mode
What sleep forced on my architecture (all good things)
- All state on disk. In-memory state dies at 1 a.m., so sessions in Redis-on-disk or DB rows, not globals. Sleep cured my lazy state management.
- Boot must be boring. Everything starts via systemd units; if wake + boot doesn’t produce a working app with zero human input, that’s a bug I fix in daylight.
- Health check = first request. I curl the domain after wake in the same cron pipeline; if boot ever fails, I know before users do.
0 7 * * * krova cubes wake web-1 && sleep 15 && curl -sf https://app.example.com/healthz || curl -sS -X POST "$ALERT_URL" -d 'text=web-1 failed to wake'
Enter fullscreen mode Exit fullscreen mode
Where the schedule lives, and the two ways it dies
A commenter caught that v1 of this post never said where the crontab runs. Outside the Cube, as above — a powered-off host can’t wake itself, and the 7 AM alert branch can’t fire from inside a stopped Cube. But that creates the mirror failure: if that host dies, the Cube never sleeps, everything looks healthy, and the bill silently goes back to 24/7. So each failure gets a checker that is alive when it fires:
- Asleep when it should be awake → the Cube is off, it can’t report itself. Only an external check notices (the 7 AM healthz + alert above).
- Awake when it should be asleep → the Cube is on, so something inside can talk:
# inside web-1 — this one MAY live on the Cube, it only needs to fire while awake
0 2 * * * awake-during-sleep-window.sh # alerts if the Cube finds itself running at 2 AM
Enter fullscreen mode Exit fullscreen mode
-
Any time — the interval watcher. Both bullets above are point samples; the bill is an interval. A stray
wakeat 3 AM slips between them and looks healthy by 7. The right observable turned out to be the state log Krova already emits: register a webhook forcube.running/cube.stopped(HMAC-signed, timestamped) and you get every transition:
POST /spaces/$SPACE/webhooks
{ "url": "https://watcher.example.com/krova",
"events": ["cube.running", "cube.stopped"] }
Enter fullscreen mode Exit fullscreen mode
Alert on any cube.running outside the 7 AM window the second it happens; sum the running→stopped intervals nightly — ~960 compute-minutes expected, more is the alarm. The bill is the ground truth; the samples were approximations.
Three checkers now: two point samples for the cheap obvious failures, one event stream for the interval. And the schedule itself lives outside anything it controls.
Objections, fairly handled
- Wake isn’t instant. First morning request waits a few seconds for boot. Correct choice for side projects / internal tools / staging with quiet hours. Wrong choice for 24/7 global SaaS — don’t sleep those.
- Sleep ≠ security while awake. It shrinks the window; it doesn’t replace the walls. The daytime Cube still needs its real properties: no public IP, own kernel, explicit ports only. Krova gives you those; sleep is just schedule math on top.
- Timezones. If your users are global, your “quiet hours” may not exist. Look at actual traffic before copying my cron.
The reframe
We treat “always on” like a moral quality. For a lot of workloads it’s just an expensive, attackable assumption. I added sleep to save money; the quietest security upgrade I’ve ever shipped was the side effect.
Give it a bed with no address. Let it sleep.
Does anyone else run real workloads on a sleep schedule? Or does powering off “production” at night terrify you? Which camp are you in, and what do your quiet hours look like? Comments open.