Even production can sleep
The cheapest server is the one that isn’t running. Everyone knows that. What took me longer to accept was that it could apply to production too.
This is how we put a multi-account AWS platform serving testing and production workloads on a working-hours schedule, production included, and cut AWS spend by 50% within a quarter. No new tooling and no rewrite. Mostly EventBridge, Lambda, and one judgement call that the whole thing rests on.
The problem
We ran three environments: testing, staging and production. All three ran 24/7. Their users did not. The people using them worked set hours, and outside those hours the environments sat there, healthy and idle, billing by the hour.
Lined up against the bill, the shape was obvious. A week has 168 hours, and the working week is a fraction of that. Most of what we were paying for was nights and weekends, and nobody was there to see them.
Turning off non-production environments overnight is a well-known move. Staging and testing were easy calls. Production was the interesting one.
Why production could sleep
The default assumption is that production is always on. For most products that’s right: users are spread across time zones, and somebody somewhere is always trying to log in.
That wasn’t our situation. Production’s users kept predictable hours, the same hours as everyone else on the platform. Availability outside those hours wasn’t buying anything, because there was no one on the other end of it.
So the question changed from “can we turn production off?” to “what does keeping it on actually get us?” The honest answer was a bigger bill and a feeling of safety. That feeling deserved to be taken seriously, but not paid for every night.
Once I was confident in that call, the rest was engineering.
The constraints
Before building anything, I wrote down what the solution had to respect:
- No budget for new tooling. Whatever we built had to use what AWS already gave us.
- Every change had to be reversible. If scheduling caused problems, we needed to go back to 24/7 without a migration.
- Teams needed predictable availability. An environment that is up “most mornings” is worse than one that is always up. The schedule had to be boring and reliable.
- Production needed safe ordering and a morning health check. Starting things in the wrong order, or assuming a clean start, wasn’t acceptable for the environment people relied on most.
The approach
The design is deliberately small. EventBridge schedules trigger Lambda functions, and those functions stop and start whole environments. Each environment is awake for a 17-hour window on weekdays and asleep the rest of the week.
The part that needs care is order. Services depend on each other, so startup runs in dependency order and shutdown runs in reverse:
startup: data layer → compute → apps
shutdown: apps → compute → data layer
Bringing the data layer up first means compute and apps find their dependencies ready when they start. Taking apps down first means nothing is left talking to a database that has already gone away.
Because it’s all schedules and functions, it’s also easy to undo. Disable the schedules, start everything once, and you’re back to 24/7. That covered the reversibility constraint without any extra work.
Guardrails
Scheduling is only safe if the exceptions are handled, so there were two guardrails.
Out-of-hours access went through the platform team. If someone needed an environment outside the window, they asked us and we woke it up. It kept the process simple, and anything running off-schedule had someone who knew about it.
Production got a post-startup health check. After production starts each morning, a health check confirms it actually came up healthy. Starting is not the same as working, and I wanted to hear about a bad start before production’s users did.
The outcome
The numbers are simple. A 17-hour window on five weekdays is 85 hours awake out of 168, about 51% of the week. If cost tracks running time, that predicts a saving of around 49%.
| Hours awake per week | Share of the week | |
|---|---|---|
| Before (24/7) | 168 | 100% |
| After (scheduled) | 85 | ~51% |
The actual result was 50% lower AWS spend within the first quarter, close to what the arithmetic predicted.
What I like most is how unremarkable it was. No new platform, no migration, no new tool for anyone to learn. Teams kept predictable environments, and production proved it was healthy every morning before anyone needed it.
What I’d do differently
It worked, but there are three things I’d change if I did it again.
Self-service wake-up. Routing out-of-hours requests through the platform team was fine at first, but it makes us a bottleneck. A Slack command or a Jenkins job that lets a team wake their own environment would remove both the wait and the interruption.
Tag-based targeting from day one. Choosing what to stop and start by tags, rather than by listing resources, means new resources join the schedule automatically. It’s much easier to start that way than to retrofit it.
Per-environment cost alerts. An alert per environment would catch drift early, such as a resource that quietly stopped being scheduled, instead of leaving it for the monthly bill to reveal.
The takeaway
The technical part of this was small. The useful part was questioning an assumption everyone, me included, treated as a rule: that production is always on. Sometimes it should be. Ours didn’t need to be, and the bill had been saying so for a while.