Skip to content

Scheduling non-production environments off-hours: the easiest 60%+ on dev/staging compute

How to schedule AWS dev and staging environments to stop off-hours: ASG scheduled actions, EventBridge Scheduler, RDS stop automation, and the 7-day restart trap.

A dev environment used 60 hours a week and billed for 168 is paying for 108 hours of nothing — roughly 64% of its compute line. Off-hours scheduling is the rare cost measure that is pure arithmetic: stop compute nights and weekends, start it before the team arrives, and the saving lands on the next invoice with no architecture change. The catch list is short but real: storage keeps billing while instances sleep, stopped RDS instances restart themselves after seven days, and one hard-coded IP assumption can turn the first automated stop into a Monday-morning surprise.

What’s happening

On-demand compute bills by the hour it runs, so hours not run are the whole trick. What stops billing when stopped: EC2 instance hours, RDS instance hours, ECS tasks scaled to zero, and per-hour capacity of most managed compute. What does not stop: EBS volumes, Elastic IPs, load balancer hours, NAT Gateway hours, and snapshots — a “stopped” environment typically still bills 10-25% of its running rate, which is why the honest claim is 60-something percent on compute, not 100.

The scheduling mechanisms, simplest first. Auto Scaling groups have native scheduled actions: set desired and minimum capacity to zero at night, restore in the morning — with time-zone support, so DST does not shift your mornings. EventBridge Scheduler runs cron-style schedules that can call AWS APIs directly (stop/start EC2, stop RDS) or trigger a Lambda for anything with ordering requirements. Instance Scheduler on AWS is the packaged AWS Solution: tag-driven schedules across accounts and regions, worth it once environments multiply. On the database side, stopping RDS works but comes with the famous asterisk: AWS automatically restarts a stopped instance after seven days, so the automation must catch and re-stop it. Newer Aurora Serverless v2 versions can pause to zero capacity on their own, which solves the same problem by different means where the engine version supports it.

What to check first

  • What actually idles. Pull two weeks of CPU and connection metrics on dev/staging — some “dev” boxes run nightly builds at 2 a.m. and must be excluded or rescheduled around.
  • Tag coverage. Tag-driven scheduling needs a reliable environment tag and a schedule tag; audit before you automate.
  • Boot order dependencies. Databases before app servers before workers; anything that needs its dependency up at start time needs ordered starts, not a single cron.
  • Static IP assumptions. Instances without an Elastic IP get a new public IP on every stop/start; anything whitelisting the old one breaks.
  • Who works odd hours. Ask before you schedule — the person testing from Vancouver at 21:00 Eastern will otherwise introduce themselves.

How to verify

Confirm the environment really idles before scheduling it:

# Is anyone using staging at night? CPU by hour, last two weeks
aws cloudwatch get-metric-statistics --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2026-06-20T00:00:00Z --end-time 2026-07-04T00:00:00Z \
  --period 3600 --statistics Maximum

# What would a schedule catch? Everything tagged for it
aws ec2 describe-instances \
  --filters Name=tag:environment,Values=dev,staging Name=instance-state-name,Values=running \
  --query 'Reservations[].Instances[].[InstanceId,InstanceType,Tags[?Key==`Name`]|[0].Value]' \
  --output table

Set up an ASG schedule and verify it registered:

# Scale to zero at 23:00 and back at 07:00, Montreal time, weekdays
aws autoscaling put-scheduled-update-group-action \
  --auto-scaling-group-name staging-asg \
  --scheduled-action-name nightly-stop \
  --recurrence "0 23 * * MON-FRI" --time-zone "America/Montreal" \
  --min-size 0 --desired-capacity 0

aws autoscaling put-scheduled-update-group-action \
  --auto-scaling-group-name staging-asg \
  --scheduled-action-name morning-start \
  --recurrence "0 7 * * MON-FRI" --time-zone "America/Montreal" \
  --min-size 2 --desired-capacity 2

aws autoscaling describe-scheduled-actions --auto-scaling-group-name staging-asg

Console paths: EC2 > Auto Scaling groups > (group) > Automatic scaling > Scheduled actions, and Amazon EventBridge > Scheduler > Schedules for the standalone-instance and RDS schedules. Scheduled-action mechanics are documented under the EC2 Auto Scaling documentation.

The runbook

  1. Start with one environment and a one-week trial. Staging, not the shared dev box everyone forgot runs the demo server. Announce the schedule, run it a week, collect complaints, then expand.
  2. Schedule ASGs with native scheduled actions. Zero at night, restore in the morning, explicit time zone. This covers the app tier with no Lambda to maintain.
  3. Cover standalone instances and RDS with EventBridge Scheduler. Stop and start on tags, either by calling the API directly or through a small Lambda when you need ordering: start the database, wait healthy, then the app tier. Remember the RDS seven-day auto-restart — a weekly “re-stop anything tagged stopped” pass closes that hole.
  4. Build the wake-up button before anyone asks. A one-click start — pipeline job, chat command, or a tiny internal page — is the difference between adoption and a revolt. Nobody accepts “the environment opens at 7.”
  5. Graduate to Instance Scheduler on AWS at fleet scale. When schedules span accounts and regions, the packaged solution’s tag model beats a folder of home-grown crons.
  6. Trim what sleeps but still bills. Once the environment stops nightly, downsize its always-on remainder: NAT Gateway in a dev VPC, oversized EBS on stopped boxes, idle load balancers — the same hunt as the unused resources checklist.

When this is the wrong move

Production is out of scope, obviously — but so is anything in the compliance path that auditors expect continuously monitored, and any environment hosting long-running integration tests that cross the stop boundary. Teams spread across time zones can shrink the window to the point of pointlessness: if dev is used 16 hours a day, the saving may not justify the operational friction. And environments about to be decommissioned should be deleted, not scheduled — sleep is not a substitute for the orphan hunt.

Common mistakes

  • Stopping RDS once and celebrating — the seven-day auto-restart resumes billing quietly until the automation re-stops it.
  • Forgetting cron time zones: a UTC schedule shifts an hour against local time at DST, and the team arrives before the environment does.
  • Killing the nightly build farm because it was tagged dev — check for scheduled workloads before scheduling sleep.
  • Counting the full instance price as the saving and forgetting that EBS, EIPs, and load balancers bill through the night.

Keeping it under control

  • Track the saving explicitly: compare the environment’s compute line month over month and publish it — visible wins protect the schedule from erosion.
  • Alert on non-production instances running outside schedule hours; exceptions become permanent silently.
  • Make the schedule tag mandatory in dev/staging IaC so new resources join the schedule at birth.
  • Review the exclusion list quarterly — the “temporary” 24/7 exception from March is still running in July.
  • Pair the schedule with rightsizing: an environment that runs 60 hours a week on smaller instances compounds both savings.

Off-hours scheduling is the cost lever with the best effort-to-savings ratio in most AWS accounts — the math is fixed, only the discipline varies. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.