Lesson 8 · 20 min read

RDS: Managed PostgreSQL

A real database that AWS backs up, patches and fails over for you — and how to connect it privately from your app server.

What you'll learn in this lesson

  1. Concept: a database somebody else looks after
  2. Why not just install Postgres on the server
  3. Who does what — you vs AWS
  4. The choices you make: class, storage, Multi-AZ
  5. Backups, snapshots and point-in-time restore
  6. Build it: a private database
  7. Connect from the app server
  8. Do not run your app as the admin
  9. Connect the Next.js app
  10. Connection limits — the outage that surprises everyone
  11. Reaching the database from your laptop, safely
  12. Day-two operations
  13. Troubleshooting table
  14. What this costs
  15. Interview corner
  16. Practice task before Lesson 9
  17. Conclusion

Concept

RDS (Relational Database Service) is an AWS service that runs databases such as PostgreSQL and MySQL for you. A relational database stores data in tables with rows and columns, and you ask it questions with SQL. With RDS you get a hostname, a port, a username and a password. You do not get a login to the machine underneath, because you do not need one. AWS looks after the server, the disk, the patches, the backups and the failover (switching to a spare copy when the main one fails).

For your application, it is an ordinary PostgreSQL database. It uses the same SQL, the same drivers and the same ORM (a library that lets code use a database through objects). The only change is that DATABASE_URL points to an AWS hostname instead of localhost.

The analogy

Installing Postgres on your EC2 server is like owning a house. You have total freedom, and you fix the roof, the plumbing and the locks. RDS is like a serviced apartment. You live the same way, but the building manager does the maintenance. You pay more each month, and you worry much less at 3 a.m.

Why not just install Postgres on the server

You can. Many small products do this, and it is fine for a hobby project. But then you own all of this:

  • Backups. Someone must write the script, schedule it and store the files somewhere else. Then comes the step that everyone skips: test a restore. A backup that you never tested is only a hope.
  • Data lives on the app server. If that machine dies, is terminated by mistake or is replaced during a deploy, your customers' data is lost with it.
  • You cannot scale out. Auto Scaling (Lesson 12) creates and removes app servers freely. This only works if the servers hold no data. A database on the same server ties you to one machine.
  • Resource fights. Postgres and Node share CPU and RAM. A slow query makes your website slow, and a busy website makes the database slow.
  • Patching and upgrades are your job, on your schedule, with your downtime.
Stateless · Vercel
Request 1→
instance Acounter = 1
Request 2→
instance Bcounter = 1
Request 3→
instance Ccounter = 1

Each instance disappears after answering. Nothing carries over.

Persistent · EC2 / Railway
Request 1Request 2Request 3
→→→
one process, 24/7counter = 1
counter = 2
counter = 3

Same process every time, so memory survives between requests.

Three requests, same counter code. Stateless: each request starts from a fresh instance. Persistent: one process remembers.

The diagram above (from Lesson 0) shows the goal: app servers that remember nothing and can be replaced at any time. This only works when the memory, which is the database, lives somewhere else. RDS is that place.

Rule of thumb

Use RDS as soon as the database holds data that you cannot afford to lose. For a real product, that is always.

Who does what — you vs AWS

This split is called the shared responsibility model. It means that AWS looks after some parts and you look after the rest. It is a very common interview question about managed services. (A managed service is one where AWS runs the machines for you.) RDS moves the line up, but your half does not disappear.

JobWhoYour part
Hardware, power, networkAWS—
Operating system and security patchesAWS (in your maintenance window)Choose the window
PostgreSQL installation and minor upgradesAWSApprove major version upgrades
Automated backups and restore machineryAWSSet retention; test restoring
Failover to a standby (Multi-AZ)AWSPay for it and turn it on
Who can reach the database (Security Group, subnet)—You
Users, passwords, permissions inside Postgres—You
Schema, indexes, slow queries—You
Sizing the instance and connection limits—You

Notice that a slow query, a missing index and a leaked password are still your job. An index is a lookup structure that makes searches fast. “Managed” does not mean that AWS fixes everything.

The choices you make: class, storage, Multi-AZ

Instance class

The instance class is the size of the database server. The names work like EC2 names, with a db. prefix. db.t4g.micro is burstable, uses an ARM Graviton CPU, and has 2 vCPU and 1 GB RAM. For learning and a low-traffic product, db.t4g.micro or db.t4g.small (2 GB) is enough. Move to db.m or db.r (r means memory-optimised) when your working set no longer fits in memory. The working set is the data you use most often. Postgres is fast when its indexes and busy rows are in RAM. It is slow when it must read from disk. So RAM is the number to watch.

Storage

  • gp3 (a type of SSD disk) is the default choice. It includes 3,000 IOPS (disk reads and writes per second), and you pay per GB. Start at 20 GB.
  • Turn on storage autoscaling (--max-allocated-storage), so a growing table never fills the disk and freezes the database. You can make storage bigger, but you can never make it smaller.
  • Turn on encryption at rest (data is scrambled on the disk) when you create the database. You cannot turn it on later. You would have to copy a snapshot with encryption and restore from it.

Multi-AZ — high availability, not a backup

Multi-AZ means AWS keeps a second, hidden copy (the standby) of the database in another Availability Zone. Every write is copied to it at the same time (synchronously). If the main database fails, RDS moves the same hostname to the standby in about one to two minutes. You change no code. (This table describes the classic Multi-AZ instance setup. A newer “Multi-AZ DB cluster” option has standbys that you can read from.)

A read replica is a different thing. It is an extra copy that you can read from, to share the load. In one line: Multi-AZ is for staying online, a read replica is for speed.

Multi-AZ standbyRead replica
PurposeSurvive a failure (availability)Share out read traffic (performance)
Can you query it?No — it sits idleYes, read-only, its own hostname
ReplicationSynchronousAsynchronous (can lag)
FailoverAutomaticManual promotion
CostRoughly 2× the instance1× per replica
Multi-AZ is not a backup

Suppose a developer runs DROP TABLE users; or a bug damages rows. The standby copies that damage within milliseconds. Multi-AZ protects against hardware failure. Only backups protect against human mistakes.

In this course we run a single-AZ database to keep the bill small. For a real product with paying customers, turn Multi-AZ on before launch. It is one setting.

Backups, snapshots and point-in-time restore

A backup is a saved copy of your data that you can restore after a failure or a mistake. A snapshot is a copy of the whole database disk at one moment. RDS gives you two kinds, and they behave differently:

Automated backupsManual snapshots
CreatedDaily, automatically, plus continuous transaction logsBy you, any time
Kept1–35 days (you choose), then deletedUntil you delete them
Restore toAny second inside the window (point-in-time restore)The moment the snapshot was taken
If you delete the databaseDeleted with it (unless you keep them)Survive
A restore never overwrites — it creates a new database

“Restore to 14:32” creates a brand-new instance with a new hostname. You then point the app at it, or copy the missing data across. Learn this before an emergency. Always take a manual snapshot before a risky migration and before you delete an instance.

Two terms every engineer should be able to explain. RPO (Recovery Point Objective) is how much data you can afford to lose. With point-in-time restore it is about five minutes. RTO (Recovery Time Objective) is how long you can be down. A restore takes tens of minutes, and a Multi-AZ failover takes about two. In one line: RPO is about lost data, RTO is about lost time. Lesson 18 practises a real restore.

Build it: a private database

  1. Create a subnet group — the list of subnets RDS may use

    A subnet group is a list of subnets where RDS is allowed to place your database. Go to RDS → Subnet groups → Create. Name it myapp-private, pick the myapp VPC and select only the two private subnets. (RDS needs two AZs, even for one database.) This step keeps the database off the internet. RDS can only place it in subnets that have no route to the Internet Gateway.

  2. Create the instance

    RDS → Create database → Standard create, PostgreSQL. The fields that matter:

    FieldSet it toWhy
    Engine versiona current major version (16 or newer)It is supported for years and gets security patches
    TemplateFree tier / Dev-TestSingle-AZ is fine while learning
    Master usernamedbadminFor you only — never for the app
    PasswordAuto-generate, or a long random oneStore it in your password manager straight away
    Instance classdb.t4g.microSmallest size; about 100 connections at most (see below)
    Storage20 GB gp3, autoscaling to 100Grows on its own instead of filling up
    Connectivitymyapp VPC, subnet group myapp-privateThe private subnets from step 1
    Public accessNoNo public IP at all — it exists only inside the VPC
    Security groupdb-sg (remove default)Accepts 5432 only from web-sg (Lesson 6)
    Backups7 days retentionPoint-in-time restore to any second in that window
    EncryptionOnFree, and can't be added later
    Deletion protectionOnAWS refuses to delete the database until you switch this off. It prevents expensive accidents

    Creating the database takes 5–15 minutes. When it is ready, copy the endpoint (the hostname of the database), which looks like myapp-db.abc123xyz.ap-south-1.rds.amazonaws.com. If you look it up from your laptop, it gives a private 10.0.x.x address. This shows that the internet cannot reach it.

Connect from the app server

The database only accepts connections from web-sg, so you must test from your EC2 instance. Log in with SSH, install the Postgres command-line client (the package postgresql-client, which gives you the psql command), and connect:

On the EC2 server
  • psql "host=<DB_HOST> port=5432 dbname=postgres user=dbadmin sslmode=require"

    It asks for the password. The prompt “postgres=>” means you are in. If it hangs, the problem is the network. See the troubleshooting table

Do not run your app as the admin

dbadmin can drop every database on the server. Suppose your web app connects as admin, and it has a SQL-injection bug (a flaw that lets an attacker run their own SQL) or a leaked .env. Then the attacker owns everything. Use the admin account only to create a limited user for the app. This is least privilege, the same idea as in Lesson 5.

inside psql, connected as dbadmin
CREATE USER appuser WITH PASSWORD 'A-LONG-RANDOM-PASSWORD';
GRANT appuser TO dbadmin;                 -- RDS needs this before dbadmin can create a database owned by appuser
CREATE DATABASE myapp OWNER appuser;      -- appuser owns this database and can only use it
REVOKE ALL ON DATABASE myapp FROM PUBLIC; -- nobody else may even connect to it

On RDS, the admin user is not a full superuser. So it must be a member of appuser (the GRANT line) before it can create a database owned by appuser. Without that line you get “must be member of role”. Make the second password a long random string without symbols, and store it in your password manager. Now you have two identities: dbadmin for you and appuser for the app.

Connect the Next.js app

Add the URL to the server's .env (still chmod 600):

.env on the server
DATABASE_URL=postgresql://appuser:THE-APP-PASSWORD@myapp-db.abc123xyz.ap-south-1.rds.amazonaws.com:5432/myapp?sslmode=require

A connection string is one line that tells your app how to reach the database. It has five parts: user : password @ host : port / database. If the password contains @ : / # ?, you must URL-encode it (write each of these symbols as a % code). This is why we made passwords without symbols.

Migrations — creating the tables

A migration is a small file of changes to the database structure, such as “add a table” or “add a column”. A migration tool applies these files in order. Whatever tool you use (Prisma, Drizzle, Knex), production has one rule: apply migrations with a deploy-only command that asks no questions. Never use the development command there.

ToolDevelopment (laptop)Production (server / CI)
Prismaprisma migrate devprisma migrate deploy
Drizzledrizzle-kit generatedrizzle-kit migrate
Knexknex migrate:makeknex migrate:latest
Never run prisma migrate dev or db push against production

The dev commands can reset the database when they find that it no longer matches your migration files (this is called drift). Deploy commands only apply migration files that are already committed and reviewed. Take a manual snapshot before every production migration that drops or rewrites a column.

Add the migration step to your deploy loop from Lesson 7, before the reload. Make it backward-compatible. The old version of the app is still running while the migration runs. So never rename or drop a column in the same release that stops using it. First add the new column. Then deploy code that works with both columns. Drop the old column in a later release. This “expand then contract” pattern lets teams change the database with no downtime.

TLS to the database

TLS is the encryption that protects data while it travels over a network. On PostgreSQL 15 and later, RDS requires TLS by default, so sslmode=require is needed. This setting encrypts the traffic, but it does not check who the server is. A convincing fake server would still be accepted.

For full safety, download the AWS RDS certificate bundle onto the server once. Give it to your Postgres driver as the trusted CA (certificate authority, the body that vouches for the server's identity), and turn on certificate checking. The driver settings are also where you set the pool size. The next section is about that number.

Connection limits — the outage that surprises everyone

A connection is one open line between your app and the database. A connection pool is a small set of connections that the app keeps open and reuses. Every PostgreSQL connection is a separate operating-system process that uses memory (a few MB each). So the database has a hard limit called max_connections. On RDS this limit is worked out from the instance's RAM. A db.t4g.micro allows roughly 100 connections (a little less in practice).

The failure looks like this:

  1. You run 4 app servers, each with a connection pool of 20 → 80 connections. Fine.
  2. Traffic grows and Auto Scaling starts 4 more servers → 160 connections wanted.
  3. The database refuses the extra ones: FATAL: remaining connection slots are reserved. The site shows errors exactly when it is busiest.
The arithmetic to do before you scale

servers × pool size per server ≤ ~80% of max_connections. With a limit of 100 you can afford, say, 8 servers × 10. Check the actual limit with SHOW max_connections; and current usage with SELECT count(*) FROM pg_stat_activity;.

The fixes, in order of effort

  • Set a sensible pool size in the app (max: 5–10), not the library default.
  • Use a bigger instance: more RAM gives a higher limit and more spare room.
  • Put a pooler in front. A pooler is a middle service that holds a small number of real database connections and shares them among thousands of app connections. Choose RDS Proxy (managed by AWS, about $0.015 per vCPU per hour of your database, with a minimum of 2 vCPUs, so roughly $22 a month at least) or run PgBouncer (a free open-source pooler) yourself. A pooler is very important with serverless functions, because each call would otherwise open its own connection. This is the usual reason that Vercel + Postgres setups hit limits.

Reaching the database from your laptop, safely

You will want to look at data with a GUI tool (TablePlus, DBeaver, pgAdmin) or run a script. The database is private. The wrong fix is to make it public. The right fix is an SSH tunnel. This is an encrypted path that goes through the app server, which is allowed to reach the database:

On your laptop
  • ssh -N -L 5433:<DB_HOST>:5432 myapp

    Forward localhost:5433 through the EC2 server to the database. Point your GUI at localhost:5433. Press Ctrl+C to close the tunnel

The traffic travels inside the encrypted SSH connection. It comes out on the server (inside the VPC) and reaches the database. Nothing is opened to the internet.

Never “just for a minute” make it public

Internet scanners find a database with a public IP and open port 5432 within minutes. Even with a strong password, you give attackers something to attack. A private subnet removes that risk completely. If a colleague needs access, give them tunnel access (or SSM port forwarding, Lesson 18), not an open port.

Day-two operations

Maintenance window

AWS applies operating-system patches and minor-version updates during a weekly window that you choose. Pick your quietest hour (for example Sunday 03:00 IST). Patching a single-AZ database causes a short outage of a few minutes. With Multi-AZ, the outage shrinks to one failover. In the console: Modify → Maintenance.

Parameter groups

You cannot edit postgresql.conf (the Postgres settings file) on RDS. Instead you attach a parameter group, which is a named set of settings. The default group is read-only. To change anything, such as logging slow queries, make your own group. Set log_min_duration_statement = 500 (milliseconds), attach the group, and reboot if the setting is static (a static setting needs a reboot to apply).

What to watch

MetricWorry whenUsually means
CPUUtilizationSustained above 70–80%Missing indexes or an undersized instance
FreeableMemoryFalling towards zeroWorking set larger than RAM
DatabaseConnectionsApproaching max_connectionsPool arithmetic wrong (previous section)
FreeStorageSpaceBelow ~20%Growth or bloat; autoscaling should catch it
ReadLatency / WriteLatencySudden spikesStorage limits or a heavy query

Lesson 17 turns these into alarms. Also turn on Performance Insights (free for seven days of history). It shows which SQL statements use most of the database time, so it answers “why is it slow?” on one screen. AWS is moving this view into CloudWatch Database Insights, so the console may show that name. The idea is the same.

Take a snapshot, and prove it restores

Before any risky change, take a manual snapshot (RDS → your database → Actions → Take snapshot). It stays until you delete it. Once, as a practice drill, use Restore to point in time to make a copy from ten minutes ago. It always restores into a new instance with a new hostname. That copy costs money until you delete it.

Troubleshooting table

SymptomLikely causeFix
psql hangs, then “connection timed out”Network path blocked: db-sg missing the rule from web-sg, or you are connecting from your laptop to a private databaseCheck db-sg inbound 5432 from web-sg. Connect from EC2, or use the SSH tunnel
FATAL: no pg_hba.conf entry … no encryptionPostgres 15+ on RDS requires TLS by defaultAdd sslmode=require to the connection string
FATAL: password authentication failedWrong user/password, or special characters not URL-encoded in DATABASE_URLTest with psql first. URL-encode @ : / # ? in the password, or generate one without them
FATAL: remaining connection slots are reservedToo many open connections (see the pooling section)Lower the app’s pool size, add pooling, or use a bigger instance
could not translate host nameTypo in the endpoint, or DNS hostnames disabled in the VPCCopy the endpoint from describe-db-instances; enable-dns-hostnames (Lesson 6)
Creation stuck in “creating” for 15+ minutesNormal for the first instance (5–15 min)Wait — the status turns to “Available” on its own
Storage full, database read-onlyFree storage hit zeroEnable storage autoscaling; alarm on FreeStorageSpace (Lesson 17)

What this costs

ItemApprox. per month (Mumbai)
db.t4g.micro, single-AZ, running 24/7~$13 (~₹1,100)
db.t4g.small~$26
20 GB gp3 storage~$3
Automated backups up to the size of your databaseFree
Manual snapshots beyond that~$0.095 per GB-month
Multi-AZ≈ double the instance and storage
RDS Proxy~$0.015 per vCPU-hour of the database, minimum 2 vCPUs (about $22+)
Pausing while you study

You can stop an RDS instance (aws rds stop-db-instance) to stop paying for compute. AWS starts it again by itself after seven days, so put a reminder in your calendar. Storage still costs money while it is stopped.

Interview corner

Multi-AZ vs read replica?

Multi-AZ is a hidden standby that is copied at the same time as the main database (synchronous). It gives automatic failover. It is for availability, not for speed, and you cannot query it. A read replica is a copy that is updated a little later (asynchronous). You can query it to take read load off the main database. It can even be in another region, and you promote it to main by hand.

Why should the database be in a private subnet if it already has a Security Group?

Defence in depth, which means several layers of protection. Someone can change a Security Group rule by mistake. A private subnet with no Internet Gateway route makes public exposure impossible by design. Two separate controls must both fail before the database is exposed.

Someone deleted a table at 14:32. How do you recover?

Use point-in-time restore to a moment just before 14:32. It creates a new instance, because restores never overwrite. Check the data, then point the app at the new instance or copy the missing data back. Multi-AZ would not help, because it copied the deletion too.

Your app suddenly returns errors after Auto Scaling added servers. The DB CPU is low. What is it?

The database ran out of connections: servers × pool size went over max_connections. Lower the pool size per server, use a larger instance, or add RDS Proxy or PgBouncer.

How do you connect to a private database from your laptop without exposing it?

Use an SSH tunnel (or SSM Session Manager port forwarding) through an instance inside the VPC that the Security Groups already allow. Never use a public IP and an open port.

How do you do a zero-downtime schema change?

Use expand then contract. First add the new columns or tables in a way that the old code still works (for example, allow empty values). Deploy code that works with both old and new structure. Move the data. Deploy code that uses only the new structure. Drop the old column in a later release.

What is RDS not doing for you?

Query and schema design, indexes, users and permissions inside the database, choosing the instance size, testing restores, and managing connections. AWS runs the server, but you still run the database.


Practice task before Lesson 9

  1. Build the database as above. From your laptop, check that the hostname gives a 10.0.x.x address and that psql hangs (it must, because the database is private). Then connect from the EC2 server. This must work.
  2. Create appuser and the myapp database. Connect as appuser and try CREATE DATABASE other;. It should be denied. Why is that good?
  3. Add DATABASE_URL to the server's .env, run your ORM's deploy migration, and load a page that reads from the database.
  4. Open an SSH tunnel and connect with a GUI client from your laptop. Then close the tunnel and confirm the connection fails.
  5. Take a manual snapshot. In the console, restore it to a new instance called myapp-db-drill, note how long it takes and that it has a different hostname, then delete the drill instance.
  6. Run SHOW max_connections;. With your app's pool size, how many app servers can you run before you hit ~80% of it?
Optional stretch

In the RDS console, turn on Performance Insights. Run a slow query on purpose (select pg_sleep(2) in a loop) and find it in the top-SQL view. This is the first tool to open in a real “the database is slow” problem.

Conclusion

RDS is PostgreSQL where AWS handles the boring and risky parts: patching, backups, failover and disks. The important parts are still yours: access, schema and sizing.

  • Private by construction: private subnets, a subnet group, no public IP, and a Security Group that trusts only the app's group.
  • Two identities: an admin for you and a limited user for the app. Use generated passwords and never type them into shell history.
  • Multi-AZ is for uptime, backups are for mistakes. Know which problem each solves, and remember that restores create a new instance.
  • Watch connections: servers × pool size must stay under the limit. Otherwise, adding servers breaks the site.
  • Reach it through a tunnel, never by opening it to the internet.

The app now has a real database, but the server is still built by hand. Next we package the app so that it runs the same everywhere: Docker.


End of Lesson 8. Next: Lesson 9 — Docker: Containerize Your Next.js and Node App.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series