What Is Amazon RDS?
Amazon Relational Database Service (Amazon RDS) is an AWS managed database service that simplifies running relational databases in the cloud. Instead of installing PostgreSQL or MySQL on a virtual machine, configuring disks, scheduling backups, and managing infrastructure maintenance yourself, you create an RDS database and AWS handles much of the underlying operational work.
Amazon RDS supports familiar relational database engines, SQL queries, transactions, indexes, and application drivers. It also provides automated backups, monitoring, storage options, replication, and high-availability configurations. But managed does not mean maintenance-free: engineers remain responsible for database schemas, query performance, connection management, application-level consistency, and recovery planning.
Table of Contents
- What Amazon RDS Provides
- How Amazon RDS Works
- High Availability, Replication, and Recovery
- Scaling and Database Performance
- RDS Security and Pricing
- Production Design Example: PostgreSQL for a SaaS API
- Ready-to-Use Example: RDS PostgreSQL with Terraform and Python
- Production Operations and Common Mistakes
- Frequently Asked Questions
- Conclusion
What Amazon RDS Provides
A relational database stores data in tables and supports relationships, constraints, indexes, and transactions. Applications use SQL to retrieve and modify records, while the database engine manages concurrency, execution plans, caching, and durable storage.
Amazon RDS manages the infrastructure required to run these engines. AWS provisions database compute and storage, performs supported maintenance operations, manages automated backups, and provides mechanisms for monitoring and recovery. Your application still connects to the database using normal database protocols and drivers.
Supported Database Engines
Amazon RDS supports several relational database engines, allowing teams to choose based on existing applications, licensing, SQL features, and operational needs.
| Engine | Typical Use Case | Considerations |
|---|---|---|
| PostgreSQL | APIs, SaaS applications, transactional systems | Rich SQL features, extensions, JSONB, indexing |
| MySQL | Web applications and transactional workloads | Widely supported drivers and tooling |
| MariaDB | MySQL-compatible applications | Engine-specific feature and compatibility differences |
| Microsoft SQL Server | Enterprise and Microsoft ecosystem applications | Licensing and edition considerations |
| Oracle Database | Enterprise database applications | Licensing, compatibility, and managed feature constraints |
| IBM Db2 | Enterprise transactional workloads | Engine-specific licensing and operational requirements |
Engine availability, supported versions, instance families, extensions, and deployment features vary. Always confirm that the specific engine version and configuration support the capabilities your application requires.
RDS vs. Self-Managed Databases
You can run PostgreSQL directly on an EC2 instance, but then your team owns operating system maintenance, database installation, storage configuration, replication setup, backups, and much of the failover process.
RDS removes a substantial portion of that infrastructure work while retaining the familiar database interface.
| Responsibility | Self-Managed on EC2 | Amazon RDS |
|---|---|---|
| Server provisioning | Application team | AWS |
| Operating system maintenance | Application team | AWS |
| Automated backup infrastructure | Application team | AWS-managed capability |
| Database schema design | Application team | Application team |
| SQL query optimization | Application team | Application team |
| Application connection management | Application team | Application team |
| High-availability infrastructure | Custom configuration | Managed Multi-AZ options |
| Major-version upgrade planning | Application team | Application team with AWS tooling |
The trade-off is reduced host-level control. You generally cannot administer the underlying operating system or use unrestricted database superuser privileges. Some extensions, configuration changes, and administrative operations are limited by the managed service.
Amazon RDS vs. Amazon Aurora
Amazon Aurora is an AWS-managed relational database engine compatible with PostgreSQL or MySQL, using a storage architecture different from standard RDS engine deployments.
Both standard RDS and Aurora provide managed database capabilities, but they differ in replication architecture, scaling features, storage behavior, and pricing.
- Standard RDS PostgreSQL/MySQL: Familiar managed engines with instance-based compute and conventional database deployment options.
- Amazon Aurora: AWS-designed database storage and replication architecture with Aurora-specific clustering and scaling capabilities.
- Aurora Serverless: Provides managed capacity scaling for supported Aurora configurations.
Aurora is not automatically faster or cheaper for every workload. Compare real query performance, recovery requirements, storage behavior, availability objectives, and total cost.
How Amazon RDS Works
An RDS database is created with a database engine, engine version, instance class, storage configuration, networking settings, and operational options. AWS provisions the resources and exposes a database endpoint that authorized applications can connect to.
Your application uses the same SQL drivers it would use with a self-hosted database. For PostgreSQL, this might be psycopg, SQLAlchemy, JDBC, or another PostgreSQL-compatible client.
DB Instances, Storage, and Endpoints
An RDS DB instance combines compute capacity with managed database storage. The instance class determines the CPU and memory resources available to the database engine.
For example, a PostgreSQL DB instance may have a hostname similar to the following:
Database endpoint:
app-db.abcdefghijkl.us-west-2.rds.amazonaws.com
Port:
5432
Database:
appdb
Connection parameters:
host=app-db.abcdefghijkl.us-west-2.rds.amazonaws.com
port=5432
dbname=appdb
user=appuser
Applications should use the managed endpoint rather than hardcoded private IP addresses. During maintenance or Multi-AZ failover, the endpoint can resolve to a different underlying database host.
RDS storage options include general-purpose SSD and provisioned-IOPS configurations. Actual performance depends on the selected storage type, provisioned throughput, IOPS, engine behavior, and workload characteristics.
Application Connections and Transactions
Amazon RDS does not change the fundamental behavior of database transactions. PostgreSQL still manages transaction isolation, row locks, MVCC, constraints, and durable commits.
Consider a basic transfer between two accounts:
BEGIN;
-- Deduct funds only when the balance is sufficient.
UPDATE accounts
SET balance_cents = balance_cents - 5000
WHERE id = 101
AND balance_cents >= 5000;
-- Application code must verify that one row
-- was updated before proceeding.
UPDATE accounts
SET balance_cents = balance_cents + 5000
WHERE id = 202;
COMMIT;
The application must verify affected-row counts and handle failures. If the debit condition is not satisfied, it must roll back rather than commit an unmatched credit.
Database correctness still depends on transaction boundaries and concurrency control. Read What Is a Database Transaction? for the underlying guarantees.
Maintenance and Engine Upgrades
RDS provides maintenance windows for supported infrastructure and database maintenance operations. Some changes can be applied immediately, while others may require a restart, brief unavailability, or a planned maintenance window.
Major database engine upgrades require application compatibility testing. Deprecated SQL features, changes in query planning, extensions, client libraries, and parameter settings can affect behavior even when the upgrade succeeds technically.
For important PostgreSQL and MySQL workloads, RDS Blue/Green Deployments may help test changes against a synchronized environment and perform a coordinated switchover, subject to feature support and configuration restrictions.
High Availability, Replication, and Recovery
Running a database on managed infrastructure does not automatically make every configuration highly available. A single-AZ RDS instance can still become unavailable during infrastructure failures or certain maintenance operations.
RDS provides multiple mechanisms for redundancy, read scaling, and data recovery. They address different problems and should not be treated as interchangeable.
Multi-AZ Deployments
A standard RDS Multi-AZ DB instance deployment maintains a primary database and a synchronous standby in another Availability Zone. When a qualifying failure occurs, RDS can fail over to the standby.
The standby in this deployment type is not available for application read queries. Its main purpose is high availability, not increasing read throughput.
RDS also supports Multi-AZ DB clusters for supported engines. These have a writer and two readable instances across three Availability Zones, providing both failover capability and additional read capacity.
During failover, existing client connections can break and in-flight transactions may fail. Applications should use bounded connection retries and safely retry operations whose outcomes are known, rather than assuming the database will switch without affecting requests.
Read Replicas and Replication Lag
Read replicas serve read-only queries and reduce pressure on the primary database. Standard RDS read replicas typically receive changes asynchronously, so their data may lag behind recently committed primary writes.
For example, an application can send product browsing queries to a replica while directing purchases and inventory updates to the primary.
import os
from contextlib import contextmanager
import psycopg
@contextmanager
def database_connection(read_only: bool = False):
# Choose an endpoint based on query consistency needs.
host = (
os.environ["DB_REPLICA_HOST"]
if read_only
else os.environ["DB_PRIMARY_HOST"]
)
with psycopg.connect(
host=host,
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
connect_timeout=5,
sslmode="require",
) as conn:
yield conn
def get_product(product_id: int):
# Suitable for a read that tolerates replica lag.
with database_connection(read_only=True) as conn:
with conn.cursor() as cursor:
cursor.execute(
"SELECT id, name, price_cents "
"FROM products WHERE id = %s",
(product_id,),
)
return cursor.fetchone()
This illustrates read routing, not a recommended production connection strategy: opening a new connection for every query is inefficient. Applications should reuse bounded connection pools, with separate pools for primary and replica endpoints when necessary.
For reads that must immediately reflect a recent write, use the primary or another explicitly designed consistency mechanism. See What Is a Read Replica? for routing, lag, and failover considerations.
Automated Backups and Point-in-Time Recovery
RDS automated backups support recovery to a point within the configured backup retention window, subject to the database engine and deployment configuration. RDS combines backup data and transactional logs as required to support point-in-time recovery.
Manual snapshots provide another recovery mechanism. They can be retained separately from the automated backup lifecycle and used to restore new database instances.
A snapshot restoration normally creates a new database rather than rewinding the existing production database in place. Recovery therefore requires planning how applications will connect to the restored instance and how any subsequent writes will be reconciled.
Backups should be tested regularly. A backup that has never been restored does not establish a reliable recovery time objective.
Scaling and Database Performance
Relational database scaling is more constrained than scaling stateless application servers. Adding API instances may increase request capacity, but it also increases connection counts and query pressure on the database.
RDS provides ways to increase resources and distribute some workloads, but SQL design and transaction behavior remain major determinants of performance.
Compute and Storage Scaling
Vertical scaling changes the database instance class to obtain more CPU, memory, or other supported resources. Depending on the modification and deployment configuration, this can require downtime or a failover.
RDS storage autoscaling can increase allocated storage when configured conditions are met. It does not automatically solve CPU saturation, memory pressure, lock contention, or inefficient SQL queries.
Read replicas can increase read capacity, but they do not distribute primary writes in the same way. Workloads that outgrow one writable database may eventually require partitioning, sharding, data-model changes, or workload separation.
Connection Pooling and RDS Proxy
Database connections consume resources. An application fleet with many processes or serverless execution environments can overwhelm a database even when individual SQL queries are reasonably fast.
Suppose 20 application instances each allow 30 database connections. The fleet can create 600 connections, which may exceed the safe concurrency of a small database instance.
RDS Proxy can help supported applications reuse backend connections, manage connection pressure, and improve resilience during certain database disruptions. However, transaction and session behavior can limit how effectively connections are multiplexed.
For application-side resource management, see What Is Connection Pooling?.
Query Optimization and Monitoring
Scaling hardware should not be the first response to every slow query. Missing indexes, large sequential scans, long transactions, blocking locks, and inefficient pagination can create bottlenecks that more CPU does not fully resolve.
For PostgreSQL, start by examining actual query plans:
-- Identify the actual execution plan.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total_cents, created_at
FROM orders
WHERE customer_id = 12345
ORDER BY created_at DESC
LIMIT 20;
-- Create an index tailored to the access pattern.
-- CONCURRENTLY must be executed outside a transaction.
CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_orders_customer_created
ON orders (customer_id, created_at DESC);
On a large production table, index creation should be scheduled and monitored for CPU, I/O, lock, and replication impact. For detailed query-planning considerations, read What Is Database Indexing?.
RDS Security and Pricing
A database contains some of an application's most important data. RDS provides infrastructure-level controls, but they must be configured alongside database accounts, application authorization, and secure handling of credentials.
Database pricing is also influenced by more than the instance class. Backup retention, provisioned storage performance, high availability, and data transfer can materially affect the monthly cost.
VPC Networking, Encryption, and Authentication
RDS databases run within AWS network configurations and use security groups to control inbound connections. Production databases should generally be placed in private subnets without public accessibility.
Use security-group references to permit database traffic only from authorized application workloads. Avoid opening PostgreSQL port 5432 or MySQL port 3306 to the internet.
Important security controls include:
- Private networking: Keep database endpoints inaccessible from the public internet.
- Security groups: Restrict database connections to trusted application identities at the network layer.
- Encryption at rest: Protect database storage using supported KMS-backed encryption.
- TLS: Encrypt database connections and validate server certificates.
- Secrets Manager: Manage and rotate supported database credentials.
- Database roles: Give application users only required SQL privileges.
- IAM authentication: Use supported temporary IAM-based database authentication where appropriate.
An RDS IAM policy granting control-plane access to describe databases does not automatically grant permission to read application tables. AWS resource permissions and database-level SQL permissions are separate layers.
RDS Pricing and Cost Drivers
For standard RDS deployments, major cost factors can include:
- Database instance class and running duration.
- Single-AZ versus Multi-AZ deployment.
- Storage allocation, storage type, and provisioned performance.
- Backup storage beyond applicable allowances.
- Additional read replicas.
- Data transfer and optional monitoring capabilities.
- Engine-specific licensing and supported purchasing discounts.
A database that runs continuously can incur substantial recurring charges even if application traffic is very low. Always estimate the complete deployment, including backups and redundancy, before choosing a production configuration.
Production Design Example: PostgreSQL for a SaaS API
Consider a SaaS platform with a FastAPI backend that manages customers, subscriptions, orders, and invoices. The application uses PostgreSQL for transactional data, Redis for caching, and SQS for asynchronous background work.
The database must support sustained traffic, recover from infrastructure failures, and preserve the correctness of financial records even when application requests are retried.
Application and Database Architecture
A production deployment can include:
- Application Load Balancer: Routes incoming API traffic to healthy backend instances.
- EC2, ECS, or Lambda: Executes the FastAPI application.
- RDS PostgreSQL Multi-AZ: Provides the transactional primary database and managed failover.
- RDS Proxy or bounded application pools: Controls connection pressure.
- Redis: Caches suitable frequently requested data outside the database.
- SQS workers: Handle notifications, reporting, and other asynchronous tasks.
- CloudWatch: Monitors database, infrastructure, and application health.
When reads become the dominant bottleneck, the team can add a read replica for reports or other read-only traffic that tolerates replication lag. Consistency-sensitive requests continue using the primary.
Database migrations are managed through a versioned application release process rather than executed independently by every application instance at startup.
Failure Scenario: Database Failover During Checkout
Suppose a user submits an order and the database primary fails during the transaction. The application receives a connection error before it knows whether the transaction committed.
Blindly retrying the entire request can create a duplicate order or payment. The system needs a stable idempotency key and a uniqueness constraint so that repeated attempts correspond to one business operation.
-- A stable request identifier prevents duplicate orders.
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
idempotency_key UUID NOT NULL UNIQUE,
customer_id BIGINT NOT NULL,
total_cents BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Retried requests cannot insert another order
-- with the same idempotency key.
INSERT INTO orders (
idempotency_key,
customer_id,
total_cents
)
VALUES (
'e2fe0d54-2b20-437d-94d7-6ed35ab137e4',
123,
4999
)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;
When the insert returns no row, the application should retrieve the existing order and verify that the request payload matches the original operation. A duplicate key must not silently authorize a different order.
After RDS completes failover, new database connections can resolve the updated endpoint. Existing connections may need to be discarded and recreated, while retry logic must distinguish connection failures from business constraint failures.
Ready-to-Use Example: RDS PostgreSQL with Terraform and Python
This example creates a private RDS PostgreSQL database using Terraform, enables encryption and automated backups, and stores the master password in AWS Secrets Manager through an RDS-managed secret.
A Python script connects using psycopg, creates a small table, and inserts records transactionally. The example assumes you already have a VPC with private subnets in at least two Availability Zones and an EC2, ECS, or other execution environment able to reach those subnets.
1. Prerequisites and Project Structure
Install Terraform, Python 3, and the AWS CLI. Configure an AWS identity with permissions to create and manage the RDS resources.
Prepare this project structure:
rds-demo/
main.tf
outputs.tf
terraform.tfvars
requirements.txt
app.py
You will provide an existing VPC ID, at least two private subnet IDs in different Availability Zones, and a security group ID assigned to the application compute resources allowed to connect to the database.
The Python program must run from an environment with network access to the private RDS endpoint and permission to read the RDS-managed Secrets Manager secret. A local laptop without VPN, private connectivity, or an appropriate tunnel cannot connect directly.
2. Provision RDS with Terraform
Create main.tf:
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-west-2"
}
variable "vpc_id" {
type = string
}
variable "private_subnet_ids" {
type = list(string)
validation {
condition = length(var.private_subnet_ids) >= 2
error_message = "Provide at least two private subnets."
}
}
variable "app_security_group_id" {
type = string
}
# Allow PostgreSQL connections from the application.
resource "aws_security_group" "database" {
name_prefix = "rds-demo-"
description = "Private PostgreSQL database"
vpc_id = var.vpc_id
tags = {
Name = "rds-demo"
}
}
resource "aws_vpc_security_group_ingress_rule" "postgres" {
security_group_id = aws_security_group.database.id
referenced_security_group_id = var.app_security_group_id
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
description = "PostgreSQL from application"
}
# RDS needs subnets in different Availability Zones.
resource "aws_db_subnet_group" "database" {
name_prefix = "rds-demo-"
subnet_ids = var.private_subnet_ids
tags = {
Name = "rds-demo"
}
}
resource "aws_db_instance" "postgres" {
identifier = "rds-demo-postgres"
engine = "postgres"
instance_class = "db.t4g.micro"
db_name = "appdb"
username = "appadmin"
# AWS generates and manages the master password.
manage_master_user_password = true
# Development-sized storage configuration.
allocated_storage = 20
max_allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true
db_subnet_group_name = aws_db_subnet_group.database.name
vpc_security_group_ids = [
aws_security_group.database.id
]
publicly_accessible = false
# Single-AZ keeps this demonstration smaller.
# Production HA can use a supported Multi-AZ option.
multi_az = false
backup_retention_period = 7
# This is a disposable development example.
# Do not use these deletion settings in production.
deletion_protection = false
skip_final_snapshot = true
auto_minor_version_upgrade = true
copy_tags_to_snapshot = true
tags = {
Environment = "development"
ManagedBy = "terraform"
}
}
The example selects the engine's default supported version rather than pinning a particular PostgreSQL release. In production, select and test an explicitly supported engine version, plan upgrades, and configure appropriate parameter groups.
The Terraform variable validation checks the number of subnet IDs but cannot ensure they belong to distinct Availability Zones. Verify that the selected subnets are private, belong to the same VPC, and span at least two Availability Zones before applying.
The database is not publicly accessible and permits connections only from resources associated with the specified application security group. Enabling Multi-AZ, deletion protection, and final snapshots is recommended for appropriate production workloads.
Create outputs.tf:
output "db_endpoint" {
description = "RDS PostgreSQL endpoint."
value = aws_db_instance.postgres.address
}
output "db_port" {
description = "PostgreSQL listener port."
value = aws_db_instance.postgres.port
}
output "db_secret_arn" {
description = "RDS-managed master credential secret."
value = aws_db_instance.postgres.master_user_secret[0].secret_arn
}
Create terraform.tfvars using identifiers from your own AWS environment:
# Replace these example identifiers before deployment.
vpc_id = "vpc-0123456789abcdef0"
private_subnet_ids = [
"subnet-0123456789abcdef0",
"subnet-0fedcba9876543210"
]
app_security_group_id = "sg-0123456789abcdef0"
Do not use the example identifiers literally. The application security group must be attached to the compute resource that will establish the database connection.
3. Connect with Python and Create Records
Create requirements.txt:
boto3
psycopg[binary]
Create app.py:
import json
import os
import uuid
import boto3
import psycopg
from psycopg.rows import dict_row
REGION = os.getenv("AWS_REGION", "us-west-2")
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.getenv("DB_NAME", "appdb")
DB_SECRET_ARN = os.environ["DB_SECRET_ARN"]
# Download the current AWS RDS CA bundle and provide
# its local path through DB_CA_BUNDLE.
DB_CA_BUNDLE = os.environ["DB_CA_BUNDLE"]
secrets = boto3.client(
"secretsmanager",
region_name=REGION,
)
def get_db_credentials() -> dict:
# Read credentials without storing passwords in code.
response = secrets.get_secret_value(
SecretId=DB_SECRET_ARN,
)
return json.loads(response["SecretString"])
def connect():
credentials = get_db_credentials()
# Verify TLS and the database server hostname.
return psycopg.connect(
host=DB_HOST,
port=5432,
dbname=DB_NAME,
user=credentials["username"],
password=credentials["password"],
sslmode="verify-full",
sslrootcert=DB_CA_BUNDLE,
connect_timeout=5,
row_factory=dict_row,
)
def initialize_schema(conn) -> None:
# DDL is committed with the surrounding transaction.
with conn.cursor() as cursor:
cursor.execute("""
CREATE TABLE IF NOT EXISTS demo_orders (
id BIGINT GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
request_id UUID NOT NULL UNIQUE,
total_cents BIGINT NOT NULL
CHECK (total_cents > 0),
created_at TIMESTAMPTZ NOT NULL
DEFAULT now()
)
""")
def create_order(conn, request_id, total_cents):
# A duplicate request must not create a second order.
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO demo_orders (
request_id,
total_cents
)
VALUES (%s, %s)
ON CONFLICT (request_id) DO NOTHING
RETURNING id, request_id, total_cents
""",
(request_id, total_cents),
)
order = cursor.fetchone()
if order is not None:
return order
# Verify that a retry uses the original amount.
cursor.execute(
"""
SELECT id, request_id, total_cents
FROM demo_orders
WHERE request_id = %s
""",
(request_id,),
)
existing = cursor.fetchone()
if existing["total_cents"] != total_cents:
raise ValueError(
"Idempotency key reused with different data"
)
return existing
def main():
request_id = uuid.uuid4()
with connect() as conn:
# psycopg commits on successful context exit
# and rolls back if an exception occurs.
initialize_schema(conn)
result = create_order(
conn=conn,
request_id=request_id,
total_cents=4999,
)
print("Created order:", result)
# Repeating the request returns the same order.
repeated = create_order(
conn=conn,
request_id=request_id,
total_cents=4999,
)
assert repeated["id"] == result["id"]
print("Idempotent retry:", repeated)
if __name__ == "__main__":
main()
The application loads credentials from Secrets Manager and verifies the RDS server certificate using a trusted CA bundle. It then creates a table and inserts an order using a unique request identifier to prevent duplicate records.
This is a small integration test, not a recommended production pool implementation. In a persistent API, use a bounded connection pool. In highly elastic Lambda workloads, consider how concurrency and RDS Proxy affect total database connection demand.
4. Deploy and Test the Database
Initialize Terraform and create the database:
# Initialize Terraform providers.
terraform init
# Check configuration.
terraform fmt
terraform validate
# Review planned infrastructure.
terraform plan
# Provision the RDS database.
terraform apply
# View the endpoint and secret ARN.
terraform output db_endpoint
terraform output db_secret_arn
# Check the RDS instance state.
aws rds describe-db-instances \
--db-instance-identifier rds-demo-postgres \
--query "DBInstances[0].{Status:DBInstanceStatus,Endpoint:Endpoint.Address}"
Once the instance is available, run the Python script from an authorized compute environment that can reach the private database endpoint and Secrets Manager.
That execution identity needs permission to call secretsmanager:GetSecretValue on the generated secret, together with any necessary KMS permissions for its encryption configuration.
# Run these commands inside an authorized VPC host
# or another environment with private connectivity.
export AWS_REGION="us-west-2"
export DB_NAME="appdb"
# Use the Terraform output values from deployment.
export DB_HOST="your-rds-endpoint"
export DB_SECRET_ARN="your-secret-arn"
# Download the AWS RDS trust bundle.
curl -fsS \
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \
-o rds-global-bundle.pem
export DB_CA_BUNDLE="$PWD/rds-global-bundle.pem"
# Install application dependencies.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Create the schema and test idempotent inserts.
python app.py
Running the application twice creates two separate orders because a new request identifier is generated on each execution. Within a single execution, the repeated insert with the same identifier returns the existing order.
For production, database changes should be managed through versioned migrations rather than executing CREATE TABLE during ordinary API requests. Schema creation is included here to keep the example self-contained.
5. Inspect and Clean Up
Review the instance configuration and resource cleanup plan:
# Inspect database configuration.
aws rds describe-db-instances \
--db-instance-identifier rds-demo-postgres \
--query "DBInstances[0].{Class:DBInstanceClass,MultiAZ:MultiAZ,Public:PubliclyAccessible,Storage:AllocatedStorage}"
# Preview deletion.
terraform plan -destroy
# Remove the disposable development database.
terraform destroy
The demonstration configuration skips final snapshots and disables deletion protection to simplify cleanup. This is intentionally unsuitable for production data.
For production, enable deletion protection, establish backup retention, use appropriate final snapshot requirements, and verify restoration procedures before accepting customer data.
Production Operations and Common Mistakes
Managed database infrastructure reduces operational work, but production reliability still depends on capacity management, query performance, monitoring, safe deployment procedures, and controlled database access.
It is especially important to monitor both database-side metrics and application-side symptoms. A database can report moderate CPU utilization while application requests suffer from lock waits, connection pool exhaustion, or inefficient queries.
Operational Metrics and Alerts
Useful RDS and application metrics include:
- CPU utilization: Sustained compute saturation.
- Freeable memory: Memory pressure and cache behavior.
- Database connections: Concurrent connection usage.
- Free storage space: Remaining storage capacity.
- Read/write latency: Storage response times.
- Read/write IOPS: Database storage activity.
- Replica lag: Freshness of replicated database data.
- Query latency: Slow SQL statements and workload changes.
- Lock waits: Time spent waiting on conflicting transactions.
- Application pool wait time: Pressure on available database connections.
RDS integrates with CloudWatch and engine-specific database telemetry. Depending on the selected engine and monitoring features, teams can also investigate query statistics, wait events, and detailed resource usage.
Common Mistakes
Common RDS mistakes include:
- Treating Multi-AZ as read scaling: A standard Multi-AZ standby does not serve reads.
- Routing all reads to replicas: Consistency-sensitive reads may return stale data.
- Using public databases unnecessarily: Database attack surface increases.
- Opening unlimited connections: Application scaling overwhelms the database.
- Increasing instance size before profiling SQL: Expensive queries remain inefficient.
- Confusing replication with backup: Bad updates and deletions can replicate.
- Ignoring connection failures during failover: Application requests fail unnecessarily.
- Skipping restore testing: Recovery assumptions remain unverified.
- Running migrations from every application instance: Concurrent schema changes create deployment risks.
- Using the master database user for all application queries: Excessive database privileges increase risk.
Production Checklist
Before running an important workload on Amazon RDS, verify that:
- The database engine and version are explicitly selected and tested.
- The database is deployed in private subnets with restricted security groups.
- Application credentials are managed securely and rotated appropriately.
- TLS is enabled with certificate verification.
- Multi-AZ configuration matches availability requirements.
- Backups, retention, and point-in-time recovery are configured.
- Restoration has been tested against realistic recovery objectives.
- Application connection pools are bounded across the entire fleet.
- Queries, indexes, and transaction durations are monitored.
- Replica lag is monitored when read replicas are used.
- Deletion protection and appropriate snapshot policies are enabled.
- Database migrations and upgrades have rollback or recovery procedures.
Frequently Asked Questions
Amazon RDS simplifies relational database operations, but some of its behavior depends on the engine, deployment type, and configuration selected.
What Is Amazon RDS Used For?
Amazon RDS is used to run managed relational databases for APIs, web applications, SaaS platforms, enterprise systems, transactional workloads, and other applications that require SQL queries, relationships, and database transactions.
What Is the Difference Between RDS and EC2?
EC2 provides virtual servers that you manage at the operating system level. Amazon RDS provides a managed relational database environment where AWS handles infrastructure provisioning, backup mechanisms, and supported maintenance operations.
You can run PostgreSQL on EC2, but your team must manage more database infrastructure and recovery responsibilities.
Is Amazon RDS Serverless?
Standard Amazon RDS DB instances use provisioned database compute resources. Amazon Aurora Serverless provides serverless capacity scaling for supported Aurora databases.
The term managed database does not automatically mean compute is billed only when SQL requests execute.
Does Amazon RDS Automatically Back Up Databases?
Amazon RDS supports automated backups with configurable retention and point-in-time recovery. The exact behavior depends on engine, deployment type, and backup settings.
Automated backups do not eliminate the need to test restores or plan recovery from application-level data corruption.
Can Amazon RDS Scale Automatically?
RDS supports storage autoscaling for eligible configurations. Compute capacity is generally scaled by modifying the instance class or using supported engine-specific capabilities.
Read replicas provide additional read capacity but do not automatically scale primary database writes.
Is Amazon RDS Free?
Eligible AWS accounts may have access to Free Tier credits or allowances, depending on current AWS offers. Otherwise, RDS charges for database compute, storage, and other applicable resources.
Multi-AZ deployments, read replicas, backups beyond applicable allowances, and certain monitoring or licensing configurations can increase total cost.
Conclusion
Amazon RDS simplifies running relational databases by managing infrastructure provisioning, supported maintenance, backups, and high-availability options. Applications retain the familiar SQL interface and can use engines such as PostgreSQL, MySQL, and SQL Server without managing their own database servers.
However, production reliability is still an application and architecture responsibility. Engineers must design schemas, optimize queries, manage connection concurrency, choose appropriate replication strategies, secure data access, and test recovery procedures. RDS reduces infrastructure work; it does not remove database engineering.
Key Takeaway: Amazon RDS is managed relational database infrastructure, not an automatically scalable database architecture. Combine suitable Multi-AZ configurations, backups, bounded connections, efficient SQL, monitoring, and tested failover behavior to build reliable production systems.
More Articles to Read
Explore these additional database and distributed systems topics to understand replication internals, transaction processing, storage behavior, and scaling challenges beyond managed database provisioning.
Comments (0)