Schedule EC2 Instances to Stop and Start with the AWS CLI
Two API calls on a timer, plus the three checks that stop a schedule from breaking an instance you care about.
An EC2 schedule is StopInstances in the evening and StartInstances in the morning. This walkthrough does it with the AWS CLI and Amazon EventBridge Scheduler, then shows how to prove each transition happened. It only works for instances whose root volume is EBS: AWS does not let you stop an instance with an instance store root volume.
Three checks before the first stop
Root device type. Run this and look for ebs in the root column:
aws ec2 describe-instances \ --instance-ids i-0abc1234def567890 \ --query "Reservations[].Instances[].{id:InstanceId,type:InstanceType,root:RootDeviceType,state:State.Name,publicIp:PublicIpAddress}" \ --output tableInstance store volumes. AWS erases the data on any instance store volume when the instance stops. If the workload writes scratch files, caches or a local database to NVMe instance storage, copy what it needs to EBS or S3 first, or leave that instance off the schedule.
Public IPv4 address. An auto-assigned public IP is released on stop and a different one is assigned on start. Anything that allowlists the address or points DNS at it will break the next morning. The private IPv4 address stays with the network interface. If the address must survive, associate an Elastic IP, and note that AWS charges for every Elastic IP address whether it is in use or not.
IAM actions you need
For a person or pipeline running the commands by hand: ec2:DescribeInstances, ec2:StopInstances, ec2:StartInstances. To limit the blast radius, scope the stop and start actions with an aws:ResourceTag condition so only tagged instances qualify (see controlling who can start and stop resources).
Run one cycle by hand
aws ec2 stop-instances --instance-ids i-0abc1234def567890aws ec2 wait instance-stopped --instance-ids i-0abc1234def567890
aws ec2 start-instances --instance-ids i-0abc1234def567890aws ec2 wait instance-running --instance-ids i-0abc1234def567890If the instance was launched with hibernation enabled, stop-instances --hibernate saves RAM to the EBS root volume instead of a clean shutdown; if hibernation fails, a normal shutdown happens.
Put it on a timer
EventBridge Scheduler can call the EC2 API directly through a universal target, so no Lambda function is needed.
- Create an execution role that EventBridge Scheduler can assume. Save this trust policy as
trust.json:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "scheduler.amazonaws.com" }, "Action": "sts:AssumeRole" } ]}aws iam create-role --role-name SchedulerEc2StartStop \ --assume-role-policy-document file://trust.jsonaws iam put-role-policy --role-name SchedulerEc2StartStop \ --policy-name ec2-start-stop \ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ec2:StopInstances","ec2:StartInstances"],"Resource":"arn:aws:ec2:*:123456789012:instance/*"}]}'- Create the evening stop. The cron format has six fields (minutes, hours, day-of-month, month, day-of-week, year), and the timezone flag takes an IANA name so the schedule follows local daylight saving:
aws scheduler create-schedule \ --name ec2-dev-stop-weeknights \ --schedule-expression "cron(0 19 ? * MON-FRI *)" \ --schedule-expression-timezone "Europe/London" \ --flexible-time-window Mode=OFF \ --target '{"Arn":"arn:aws:scheduler:::aws-sdk:ec2:stopInstances","RoleArn":"arn:aws:iam::123456789012:role/SchedulerEc2StartStop","Input":"{\"InstanceIds\":[\"i-0abc1234def567890\"]}"}'- Create the morning start the same way with
--name ec2-dev-start-weekdays,cron(0 7 ? * MON-FRI *)and the target ARN ending inec2:startInstances.
To undo, delete both: aws scheduler delete-schedule --name ec2-dev-stop-weeknights.
What still bills while it is stopped
- You are not charged for instance usage or data transfer while the instance is stopped.
- Every attached EBS volume keeps billing for its provisioned storage.
- Elastic IP addresses keep billing.
- Each start begins per-second billing again, with a one-minute minimum per start.
So the saving is the compute rate for the hours it is off, not the instance’s whole line on the bill.
Prove it ran
aws ec2 describe-instances --instance-ids i-0abc1234def567890 \ --query "Reservations[].Instances[].State.Name" --output textaws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=StopInstancesThe first should print stopped after 19:00 and running after 07:00. The CloudTrail lookup covers the last 90 days and shows which principal made each call, which is how you tell the schedule’s stops apart from a colleague’s.
Doing the same in ZopNight
ZopNight starts and stops EC2 instances from a schedule that carries an IANA timezone and cron start and stop expressions, attached to single instances or to a resource group. Someone working late can set a force-on override with an expiry instead of editing the schedule. The account must be connected with read and write access; a read-only connection still gets discovery and recommendations but never issues a stop. The EC2 scheduling page covers what the schedule does to each instance.