DEV Community

KloudAudit
KloudAudit

Posted on

How to cut your AWS RDS costs by 65% in 20 minutes (without touching production)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)