This is the single most common finding in every cloud bill I audit.
Dev and staging RDS instances running 24/7 at production size.
A db.r5.xlarge costs $876/month running continuously. If your team uses it 8 hours a day on weekdays, you're paying for 720 hours and using 170. That's a 76% waste rate on a single instance.
The fix takes 20 minutes and touches nothing in production.
The problem in numbers
db.r5.xlarge on-demand: $876/month (720 hrs)
db.r5.xlarge scheduled: $306/month (252 hrs — weekdays 8am-7pm)
Monthly saving: $570/month
Annual saving: $6,840/year
Multiply that by 2-3 dev/staging environments and you're looking at $15,000–$20,000/year on instances that sleep most of the time.
Step 1: Find your idle RDS instances
# List all RDS instances with their sizes and status
aws rds describe-db-instances \
--query 'DBInstances[*].{ID:DBInstanceIdentifier,Class:DBInstanceClass,Status:DBInstanceStatus,MultiAZ:MultiAZ}' \
--output table
Look for anything that:
- Has "dev", "staging", "test", or "qa" in the name
- Is the same instance class as production
- Has Multi-AZ enabled (almost never needed for dev)
Step 2: Create the IAM role
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role \
--role-name rds-scheduler-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name rds-scheduler-role \
--policy-arn arn:aws:iam::aws:policy/AmazonRDSFullAccess
Step 3: Create the Lambda function
# rds_scheduler.py
import boto3
import os
def handler(event, context):
rds = boto3.client('rds')
action = event.get('action') # 'start' or 'stop'
instances = os.environ.get('RDS_INSTANCES', '').split(',')
for instance_id in instances:
if not instance_id.strip():
continue
try:
if action == 'stop':
rds.stop_db_instance(DBInstanceIdentifier=instance_id.strip())
print(f'Stopped: {instance_id}')
elif action == 'start':
rds.start_db_instance(DBInstanceIdentifier=instance_id.strip())
print(f'Started: {instance_id}')
except Exception as e:
print(f'Error on {instance_id}: {e}')
return {'status': 'done', 'action': action, 'instances': instances}
Deploy it:
zip scheduler.zip rds_scheduler.py
aws lambda create-function \
--function-name rds-scheduler \
--runtime python3.12 \
--role arn:aws:iam::YOUR_ACCOUNT_ID:role/rds-scheduler-role \
--handler rds_scheduler.handler \
--zip-file fileb://scheduler.zip \
--environment "Variables={RDS_INSTANCES=your-dev-db,your-staging-db}"
Step 4: Create the EventBridge schedules
# Stop at 7pm every weekday
# Adjust UTC offset for your timezone — CET/Warsaw is UTC+1, so 18:00 UTC = 7pm local
aws events put-rule \
--name "StopDevRDS" \
--schedule-expression "cron(0 18 ? * MON-FRI *)" \
--state ENABLED
# Start at 8am every weekday
aws events put-rule \
--name "StartDevRDS" \
--schedule-expression "cron(0 7 ? * MON-FRI *)" \
--state ENABLED
# Get your Lambda ARN
LAMBDA_ARN=$(aws lambda get-function \
--function-name rds-scheduler \
--query 'Configuration.FunctionArn' \
--output text)
# Add targets
aws events put-targets \
--rule StopDevRDS \
--targets "Id=1,Arn=$LAMBDA_ARN,Input='{\"action\":\"stop\"}'"
aws events put-targets \
--rule StartDevRDS \
--targets "Id=1,Arn=$LAMBDA_ARN,Input='{\"action\":\"start\"}'"
# Allow EventBridge to invoke Lambda
aws lambda add-permission \
--function-name rds-scheduler \
--statement-id allow-eventbridge \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn $(aws events describe-rule --name StopDevRDS --query 'Arn' --output text)
Step 5: Downsize the instance too
While you're here — dev doesn't need a db.r5.xlarge. Drop to db.t3.medium for another 70% saving:
aws rds modify-db-instance \
--db-instance-identifier your-dev-db \
--db-instance-class db.t3.medium \
--apply-immediately
Combined result:
db.r5.xlarge 24/7: $876/month (original)
db.r5.xlarge scheduled: $306/month (schedule only)
db.t3.medium scheduled: $89/month (schedule + downsize)
Annual saving: $9,444/year — from one dev database
Step 6: Add a wake-up Slack command (optional)
Your team will occasionally need the DB outside hours. Give them a self-service way to start it:
# Create a Lambda function URL
aws lambda create-function-url-config \
--function-name rds-scheduler \
--auth-type NONE
Then in Slack: Apps → Slash Commands → /start-devdb → point to the URL with body {"action": "start"}.
Engineers can start the DB themselves without waiting for someone with AWS console access.
What this looks like in real teams
I've helped teams set this up in about 20 minutes. The conversation afterwards is always the same:
"I can't believe we were paying for that for 14 months."
Not because they didn't know. Because nobody sat down and looked.
This is one of 18 checks in KloudAudit — a free self-guided cloud cost audit that walks through all of these systematically. No AWS credentials required. Takes 15 minutes.
Quick reference
| Action | Command |
|---|---|
| Find idle RDS | aws rds describe-db-instances --query ... |
| Stop instance now | aws rds stop-db-instance --db-instance-identifier NAME |
| Start instance now | aws rds start-db-instance --db-instance-identifier NAME |
| Check instance state | aws rds describe-db-instances --db-instance-identifier NAME --query 'DBInstances[0].DBInstanceStatus' |
What's your team's current RDS setup — scheduled or running 24/7?
Drop your setup in the comments — genuinely curious how common this is.
Top comments (0)