Stop RDS on a schedule — and survive the 7-day forced restart
Stopping non-production databases overnight is obvious money. The part that catches people is that RDS will not stay stopped: at seven days AWS starts the instance back up. Here is what actually bills while an instance is stopped, why the 7-day rule breaks the naive script, and the approach that holds up.
What "stopped" actually saves
Stopping an RDS instance stops the compute charge — the instance-hour rate. It does not stop everything:
- You stop paying for: instance-hours. In us-east-1 that is roughly
$0.082/hour fordb.t3.mediumand$0.178/hour fordb.m5.large(PostgreSQL, single-AZ). - You keep paying for: provisioned storage (per GB-month), any Provisioned IOPS, and backup storage. A stopped instance is cheaper, not free.
For a Multi-AZ deployment the instance-hour saving roughly doubles, because you are no longer paying for the standby compute either. The storage side is unchanged.
Read replicas cannot be stopped. Aurora is a different model — you stop the cluster, not an instance, and the same 7-day limit applies to it. This guide is about standard RDS instances; the reconciliation pattern below applies to all three.
The 7-day forced restart
This is the behavior that breaks homegrown scheduling:
Amazon RDS keeps an instance in the stopped state for a maximum of seven
days. At the limit, RDS automatically starts the instance to apply
pending maintenance. It does not ask, and it does not re-stop afterward.
A nightly cron that stops at 19:00 and starts at 08:00 never hits this — the instance is only ever
stopped for ~13 hours. But the moment someone stops a database "for now" (a paused project, a
staging env between test cycles, a long holiday), the single-shot approach fails silently: seven
days later RDS starts it, and it runs — and bills the full instance-hour rate — until a human
notices. For a db.m5.large that is $0.178/hour, about
$128/month, quietly resuming.
Why a single stop is the wrong primitive
The failure is architectural, not a bug in anyone's script. "Stop this instance" is an imperative — it fires once and assumes the world stays put. AWS's own maintenance behavior violates that assumption. The correct primitive is declarative: "this instance should be stopped during these hours," enforced repeatedly. When the 7-day restart brings the instance back inside a scheduled-off window, the next reconciliation notices it is running when it should not be, and stops it again.
A durable approach: reconcile, don't fire-and-forget
Run a small Lambda on an EventBridge schedule (say every 10 minutes). On each run it resolves, for the current time, whether each tagged instance should be running, and enforces that — handling the transient states RDS returns so it does not throw when an instance is mid-transition.
# enforce_rds.py — invoked by an EventBridge rule every 10 minutes
import boto3
rds = boto3.client("rds")
def desired_running(tags: dict, now) -> bool:
# parse your schedule tag, e.g. "9-17 mon-fri", in the resource's timezone
# return True if `now` falls inside a working window, else False
...
def handler(event, context):
now = ... # timezone-aware current time
paginator = rds.get_paginator("describe_db_instances")
for page in paginator.paginate():
for db in page["DBInstances"]:
tags = {t["Key"]: t["Value"]
for t in rds.list_tags_for_resource(
ResourceName=db["DBInstanceArn"])["TagList"]}
if "uptime:schedule" not in tags:
continue
state = db["DBInstanceStatus"]
want = desired_running(tags, now)
db_id = db["DBInstanceIdentifier"]
# Only act from a stable state; ignore modifying/backing-up/etc.
if want and state == "stopped":
rds.start_db_instance(DBInstanceIdentifier=db_id)
elif not want and state == "available":
# this branch also re-stops after the 7-day forced start
rds.stop_db_instance(DBInstanceIdentifier=db_id)
The important line is the last branch: because it runs on every interval, it re-stops an instance that the 7-day rule brought back. A one-shot script has no equivalent. The parts that need care in production:
- Transient states. You can only
stopfromavailableandstartfromstopped. Skipmodifying,backing-up,starting,stopping, and let the next interval converge. - Start latency. An RDS instance takes minutes to become
available. Fire the on-window early enough that the database is ready before the app tier needs it. - Maintenance windows. The forced start exists to apply maintenance. If you re-stop immediately, pending maintenance may keep re-triggering; leave headroom, or let a weekly on-window absorb it.
- Multi-AZ & Aurora. Same reconcile loop, different call shape (
stop_db_clusterfor Aurora).
This is the model Uptime Scheduler runs inside your account: a continuous reconciler rather than a cron that hopes the instance stays put. It is also why the 7-day restart is a non-event for us — and for AWS's own Instance Scheduler, which reconciles the same way. See the comparison if that is the tool you are weighing.
FAQ
Does a stopped RDS instance still cost money?
Yes. You stop paying instance-hours (e.g. ~$0.082/hr for db.t3.medium,
$0.178/hr for db.m5.large in us-east-1) but keep paying for storage,
Provisioned IOPS, and backups.
Why does my stopped RDS instance start itself again?
RDS keeps an instance stopped for at most seven days, then auto-starts it to apply maintenance. Single-shot stop scripts get caught by this; a reconciling scheduler re-stops it.
How do you stop RDS reliably on a schedule?
Reconcile desired state on an interval instead of firing one stop. Each run re-stops anything that is running when it should be off — including after the 7-day forced start.
Find the databases worth scheduling first
upscan reports idle RDS instances and what
they cost — run upscan --resources rds --region us-east-1. Free CLI, no signup;
start from the number.