Managed Databases
By the end of this lesson
Run a database without operating the server yourself.
A managed database service gives you a database endpoint instead of a server. The provider runs the engine, applies its patches, takes backups on a schedule, and can move the database to standby hardware when the primary fails. You connect with a connection string and write queries, exactly as before.
The work that disappears is the work nobody enjoys and everybody postpones: patching a database engine, verifying that last night's backup actually completed, and building a failover you hope never runs. The work that remains is the work that needs your knowledge of the data — the schema, the indexes, the queries, and who is allowed to connect.
What the service actually promises:
- Managed database
- A database engine operated by the provider. Azure SQL Database and Azure Database for PostgreSQL; Amazon RDS and Aurora. The engine is usually the same software you would install yourself, with administrative access removed.
- Automated backup
- Backups taken on the provider's schedule and kept for a retention period you configure. Typically a full backup plus a continuous log, which is what makes the next entry possible.
- Point-in-time restore
- Restoring to a specific moment inside the retention window, usually to a new database rather than over the existing one. This is your recovery from a bad migration or a mistaken delete.
- Failover
- Promoting a standby to primary when the primary is unhealthy. Automatic on most managed tiers, and it costs a brief interruption — open connections drop and your application has to reconnect.
- Read replica
- A read-only copy kept in step asynchronously. Useful for reports and dashboards, and it lags behind the primary by a small and variable amount, so it is not a place to read data you just wrote.
- Maintenance window
- The period in which the provider may apply updates that require a restart. You choose when, within limits. You do not choose whether.
The same PostgreSQL database, self-operated and managed:
| Database you operate | Managed database service | |
|---|---|---|
| Called | A database installed on a virtual machine | Azure Database for PostgreSQL or Azure SQL Database; Amazon RDS or Aurora |
| Engine patching | Yours to schedule, test and apply | The provider's, inside a maintenance window you pick |
| Backups | A job you write, and a restore you have to test yourself | Automatic, with point-in-time restore inside the retention period |
| Failover | Yours to build, configure and rehearse | Usually a tier setting, with a short connection interruption when it happens |
| Engine version timing | You decide, and you can stay on an old version as long as you accept the risk | The provider decides the window. Minor versions arrive on their schedule; major upgrades have deadlines |
| Server configuration | Every setting, plus extensions and filesystem access | An approved subset of parameters. Some settings and extensions are unavailable |
| Superuser access | Yours | No. You get an administrative role with restrictions |
| Cost shape | Lower listed price, plus the operational time nobody costs | Higher listed price, and the operational time is included |
One constraint catches almost everyone moving to a managed database: connections are limited, and the limit is tied to the tier you bought. A small tier may allow a couple of hundred connections in total, and that total covers your API replicas, background jobs, reporting tools and whoever is connected with a query window open.
Opening a connection is also not cheap. It involves a network round trip, a TLS handshake and authentication — commonly tens of milliseconds. Doing that per request adds latency to every request for no benefit.
A connection pool solves both problems. The pool opens a small number of connections, keeps them open, and lends one to each request that needs it. Your code opens and closes a connection as usual; the pool intercepts and hands back a live one. Modern data access libraries pool by default, which means the mistake is rarely forgetting to pool — it is failing to notice that the pool size multiplied by the replica count exceeds what the database allows.
// Budget: the database tier allows 200 connections.
// Reserve some for jobs and humans, then divide the rest by the replica ceiling.
// 200 total - 40 reserved = 160, across 8 replicas = 20 per replica.
var connectionString = new NpgsqlConnectionStringBuilder
{
Host = builder.Configuration["Database:Host"],
Database = "employees",
Username = "employees_api",
Password = employeesDbPassword, // from the secret store, never from a file
SslMode = SslMode.Require,
Pooling = true,
MinPoolSize = 2,
MaxPoolSize = 20,
Timeout = 15, // seconds to wait for a free pooled connection
CommandTimeout = 30 // seconds a single query may run
}.ConnectionString;
builder.Services.AddDbContext<EmployeesContext>(options =>
options.UseNpgsql(connectionString, npgsql =>
{
// Failover drops open connections. Retry the transient case,
// so a standby promotion is a blip rather than an outage.
npgsql.EnableRetryOnFailure(maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(5),
errorCodesToAdd: null);
}));- The arithmetic in the comment is the part worth copying. Pool size is a per-process ceiling, so the number that matters is pool size times maximum replicas, plus everything else that connects. Autoscaling makes this easy to get wrong, because the replica count that breaks the limit only appears under load.
- Reserving headroom keeps a path open for migrations, background jobs and a human investigating an incident. A database at its connection limit refuses new connections, including the one you need to diagnose it.
- Timeout is how long a request waits for a free connection from the pool. A short value turns pool exhaustion into fast, visible failures rather than requests that hang until the client gives up. CommandTimeout is a separate limit on one query, and leaving it unbounded lets a single bad query hold a connection indefinitely.
- EnableRetryOnFailure exists because managed databases move. Failover and maintenance both drop connections, and retrying the transient errors turns a few seconds of disruption into something users may not notice. It retries only errors the provider marks as transient, so it will not mask a genuine query fault.
- The password comes from a secret store, which the next lesson covers. It is worth noting here because a connection string is the most common place a credential ends up committed.
Before you call a managed database production-ready:
Check the retention period is long enough
Ask how long it would take someone to notice a quiet data corruption. If that is longer than your retention window, the restore you need will not exist. Default windows are often shorter than people assume.
Restore it once, for real
Restore to a point in time an hour ago, into a new database, and query it. You are measuring two things: that it works, and how long it takes. Both belong in your documentation.
Close the network
Turn off public access if the service allows it, and permit connections only from your application's network. A managed database reachable from the internet with only a password in front of it is a poor position to be in.
Give each workload its own login
The API needs to read and write rows. The reporting tool needs to read. Migrations need to change schema. Three logins, three sets of permissions, and the reporting tool can no longer drop a table.
Do the connection arithmetic
Pool size times maximum replicas, plus jobs and humans, must sit under the tier's limit with room to spare. Write the calculation in a comment next to the configuration so the next person changing the replica count sees it.
Summary
- A managed database hands the provider engine patching, backups and failover, and leaves you the schema, queries and access
- The cost is less configuration control, no superuser, and upgrade timing you influence rather than decide
- Point-in-time restore protects against bad changes; replicas and failover do not, because they copy them
- A backup is a claim until you restore it and time how long that took
- Connections are a limited resource: pool size times replica count, plus jobs and humans, must fit under the tier limit
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Do the connection arithmetic
Your database tier allows 120 connections. The employees API pools 25 per replica and scales up to 6 replicas. A nightly export opens 5, and two people usually have a query tool connected.
Work out whether this holds, and decide what you change.
Show solution
Six replicas at 25 is 150, before the export and the humans. The limit is 120, so under full load the API cannot open the connections it wants and requests start failing at the pool timeout.
It passes testing because you rarely test at maximum replicas. The failure arrives on the busiest day, which is also the day the autoscaler adds the replicas that break it.
Reducing the pool to 15 gives 90 across six replicas, leaving 30 for the export, the humans and a margin. A pool of 15 serves far more than 15 concurrent requests, because a request holds a connection only while a query runs.
Raising the tier is the other option, and it is the right one if queries genuinely queue on the smaller pool. Measure the wait for a pooled connection before paying for a bigger tier — the usual cause of long waits is a slow query holding connections, not too few of them.
Think about it
Which failure does each feature cover?
Match each failure to what protects you: the primary database's hardware fails; a migration adds a NOT NULL column and truncates data; a reporting query overloads the database during working hours; the provider upgrades the engine version.
Show solution
Hardware failure is covered by automatic failover to a standby. Connections drop, retry logic hides most of it, and no data is lost on a synchronous standby.
The bad migration is covered by point-in-time restore, and only if the retention window reaches back far enough and somebody notices. Failover is useless here — the standby applied the same migration.
The reporting query points to a read replica, so the reports run against a copy and leave the primary to serve the API. Accept that the replica lags.
The engine upgrade is the maintenance window, and what protects you is retry-on-transient-failure plus knowing the window. Nothing prevents it, which is the trade-off you accepted for not patching the engine yourself.
Saved in this browser only.