---
title: 'Managed Relational Database'
source: 'https://academia.sh/en/courses/cloud-storage/managed-relational-database'
course: 'Storage and Data in the Cloud'
language: en
updated: '2026-09-13T05:07:36+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Splits 28 decisions in a managed relational database into three sets — delegated, kept, and inaccessible — and counts each'
    - 'Separates which kept decisions cannot be reversed and places the major version upgrade and collation choice in that set'
    - "Splits the failover outage into the provider's fixed share and the application's variable share, and compares dropped requests across five settings"
    - "Computes how the snapshot format's inaccessibility forces the exit into a logical export, as time and egress fee"
---

# Managed Relational Database

An item-by-item comparison of a self-run relational database against the managed service: how many of 28 decisions go to the provider, how many stay with you, and how many become inaccessible altogether; how many of the kept items turn out to be one-way; the failover window split into the provider's fixed share and the application's share, with dropped requests measured across five cache lifetimes; and the data's exit costed as logical export time plus egress fee.

The previous topic settled where objects go and who can reach them, and for how long, with
numbers. One thing was left unasked: the **queried** data — where meter readings are
summarized, work orders change state, and billing documents are linked — still sits in a
relational database set up by hand. That database's machine may be rented, but the person
taking the backup, applying the patch, and promoting the standby to primary during an
outage is still a person.

This lesson's question is not "is the managed service better." What gets measured is
narrower and more concrete: when the database is rented, **which decisions change hands**.
A decision can go to one of three places — it is delegated to the provider, it stays with
you, or it lands with no one at all: it becomes inaccessible. That third set is this
lesson's real subject, because a database you run yourself has no such set.

## The Decision Splits into Three Sets

In a database you run by hand, all 28 items are kept: which disk the hardware runs on,
what the kernel setting is. The managed service splits this list. **Delegated** items are
now the provider's job, and the provider answers for the outcome. **Kept** items remain as
settings; you can change them. **Inaccessible** items are neither delegated nor kept: they
have been removed from the settings surface, the provider has picked a number, and that
number is not visible.

This split is not abstract. On an outage night, an item in the inaccessible column decides
how long the night runs, and that item cannot be touched.

The catalog was built on a single criterion: what it takes to change the item. If a
request changes it, it is counted as kept; if a provider notification changes it, it is
counted as delegated; if nothing changes it, it is counted as inaccessible. The line is
thin for some items — backup frequency is kept but how the backup is taken is delegated,
growing storage is kept but shrinking it is inaccessible. This is not a flaw in the
catalog; it is the thing being measured: a managed service does not hand over decisions as
one block, it **splits every item on its own**.

## Mechanics

**MD1 — the fictional regional measurement network's work-order database is 220 GB and
takes 1,400 requests a second.** All numbers are fictional. **MD2 — the health check runs
every 5 seconds, three consecutive failed checks trigger a failover, and the first check
lands 2.4 seconds after the fault.** **MD3 — promoting the replica to primary takes 20
seconds.** **MD4 — on the application side, the endpoint name's resolver cache lives for T
seconds; the renewal moments of sixteen application instances are spread evenly across the
T interval.** **MD5 — each instance carries a pool of 120 connections, one connection is
established in 25 ms, and eight connections can be established concurrently.** **MD6 —
the egress fee is a fictional unit: 0 within a region, 2 across regions, 9 units/GB to the
internet.** MD2 and MD3 sit on the provider's side, MD4 and MD5 on the application's; the
measurement is built to expose this line.

```js
// md/model.mjs — MANAGED RELATIONAL DATABASE model. The decision catalog and failover
// timeline defined over the fictional regional measurement network's work-order database.
// Not a real product or measurement; there is no randomness, every run yields the same numbers.
// Third column: who holds the decision in the managed service. Fourth column: is it reversible.
export const DECISION = [
  ["hardware selection and placement", "delegated", true],
  ["operating system patching", "delegated", true],
  ["minor version patching", "delegated", true],
  ["backup format", "delegated", true],
  ["snapshot storage", "delegated", true],
  ["point-in-time restore infrastructure", "delegated", true],
  ["buffer pool size", "delegated", true],
  ["replication layout", "delegated", true],
  ["major version upgrade decision", "kept", false],
  ["patch window", "kept", true],
  ["backup frequency", "kept", true],
  ["backup retention period", "kept", true],
  ["manual failover trigger", "kept", true],
  ["connection limit", "kept", true],
  ["planner cost settings", "kept", true],
  ["installing a listed extension", "kept", true],
  ["storage growth", "kept", false],
  ["placement region selection", "kept", false],
  ["character set and collation", "kept", false],
  ["shell access", "inaccessible", null],
  ["file system layout", "inaccessible", null],
  ["data directory path", "inaccessible", null],
  ["failover threshold", "inaccessible", null],
  ["write-ahead log threshold", "inaccessible", null],
  ["extension not on the list", "inaccessible", null],
  ["storage shrink", "inaccessible", null],
  ["direct access to log files", "inaccessible", null],
  ["kernel parameter", "inaccessible", null],
];

// Failover timeline. The provider's two steps are fixed; the third step depends on the
// application's resolver-cache lifetime (T) and VARIES PER INSTANCE.
export const HEALTH_INTERVAL = 5, HEALTH_PHASE = 2.4, THRESHOLD = 3, UPGRADE = 20;
export const INSTANCES = 16, REQUESTS = 1400, POOL = 120, CONNECT_MS = 25, CONCURRENT = 8;

export const detect = () => HEALTH_PHASE + (THRESHOLD - 1) * HEALTH_INTERVAL;
export const poolSetup = () => (POOL * CONNECT_MS) / CONCURRENT / 1000;

// Each application instance renews its own cache at a different moment, spread evenly
// across T. An instance learns the new endpoint at the FIRST renewal after the upgrade ends.
export function failover(T) {
  const readiness = detect() + UPGRADE;
  const instances = [];
  for (let i = 0; i < INSTANCES; i += 1) {
    const phase = (i * T) / INSTANCES;
    const learn = phase + T * Math.max(0, Math.ceil((readiness - phase) / T));
    instances.push(learn + poolSetup());
  }
  const dropped = instances.reduce((t, x) => t + x * (REQUESTS / INSTANCES), 0);
  return { readiness, worst: Math.max(...instances),
    average: instances.reduce((a, b) => a + b, 0) / INSTANCES, dropped: Math.round(dropped) };
}
```

```js
// md/measure.mjs — delegated decision count and the failover window
import { DECISION, failover, detect, poolSetup, INSTANCES, REQUESTS, POOL, UPGRADE } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const count = (k) => DECISION.filter(([, v]) => v === k).length;
console.log(`decision catalog: ${DECISION.length} items. In a database you run yourself, all are kept.\n`);
console.log("in the managed service  | items | share | example");
console.log("------------------------|-------|-------|--------------------------------------------");
for (const k of ["delegated", "kept", "inaccessible"]) {
  const list = DECISION.filter(([, v]) => v === k).map(([a]) => a);
  console.log(`${k.padEnd(23)} | ${s(list.length, 5)} | ` +
    `${s((100 * list.length / DECISION.length).toFixed(1) + "%", 5)} | ${list.slice(0, 2).join(", ")}`);
}
const irreversible = DECISION.filter(([, v, g]) => v === "kept" && g === false);
console.log(`\nof the ${count("kept")} kept items, ${irreversible.length} are one-way:`);
for (const [a] of irreversible) console.log(`  - ${a}`);
console.log(`${count("inaccessible")} inaccessible items are settings when you run it yourself; here there are none.\n`);

console.log(`failover: health check detection ${detect().toFixed(1)} s, upgrade ` +
  `${UPGRADE} s -> provider share ${(detect() + UPGRADE).toFixed(1)} s (fixed).`);
console.log(`pool setup ${(poolSetup() * 1000).toFixed(0)} ms, ${INSTANCES} instances x ${POOL} ` +
  `connections = ${INSTANCES * POOL} new connections hit the new primary at once.`);
console.log(`request rate ${REQUESTS}/s; all of it drops during the failover.\n`);
console.log("cache TTL T     | provider share | app share     | outage (avg)  | outage (worst)    | dropped req");
console.log("----------------|----------------|---------------|---------------|-------------------|-------------");
for (const T of [5, 15, 30, 60, 120]) {
  const r = failover(T);
  console.log(`${s(T + " s", 15)} | ${s(r.readiness.toFixed(1) + " s", 14)} | ` +
    `${s((r.average - r.readiness).toFixed(1) + " s", 13)} | ${s(r.average.toFixed(1) + " s", 13)} | ` +
    `${s(r.worst.toFixed(1) + " s", 17)} | ${s(r.dropped, 11)}`);
}
const a = failover(5), b = failover(120);
console.log(`\nrun-independent: provider share fixed at ${a.readiness.toFixed(1)} s; as T goes ` +
  `from 5 to 120, outage rises ${a.average.toFixed(1)} -> ${b.average.toFixed(1)} s, dropped requests ` +
  `${a.dropped} -> ${b.dropped}.`);
console.log(`the failover threshold is inaccessible, so detection time cannot be shortened; the only ` +
  `thing that can be shortened is T on the application side.`);

// Exit: because the snapshot format is inaccessible, data can leave only through a LOGICAL export.
const SIZE_GB = 220, READ_MBS = 40, LOAD_MBS = 26, RATE = { "within region": 0, "cross-region": 2, "internet": 9 };
const hours = (mbs) => (SIZE_GB * 1024) / mbs / 3600;
console.log(`\nexit: ${SIZE_GB} GB of work-order data, logical export at ${READ_MBS} MB/s, ` +
  `load at ${LOAD_MBS} MB/s (indexes are rebuilt).`);
console.log(`time: export ${hours(READ_MBS).toFixed(2)} h + load ${hours(LOAD_MBS).toFixed(2)} h = ` +
  `${(hours(READ_MBS) + hours(LOAD_MBS)).toFixed(2)} h. The snapshot cannot travel: the format sits in the inaccessible column.`);
console.log("destination      | egress fee (fictional unit/GB) | total (fictional unit)");
console.log("-----------------|--------------------------------|------------------------");
for (const [name, rate] of Object.entries(RATE))
  console.log(`${name.padEnd(16)} | ${s(rate, 30)} | ${s(rate * SIZE_GB, 20)}`);
```

```
decision catalog: 28 items. In a database you run yourself, all are kept.

in the managed service  | items | share | example
------------------------|-------|-------|--------------------------------------------
delegated               |     8 | 28.6% | hardware selection and placement, operating system patching
kept                    |    11 | 39.3% | major version upgrade decision, patch window
inaccessible            |     9 | 32.1% | shell access, file system layout

of the 11 kept items, 4 are one-way:
  - major version upgrade decision
  - storage growth
  - placement region selection
  - character set and collation
9 inaccessible items are settings when you run it yourself; here there are none.

failover: health check detection 12.4 s, upgrade 20 s -> provider share 32.4 s (fixed).
pool setup 375 ms, 16 instances x 120 connections = 1920 new connections hit the new primary at once.
request rate 1400/s; all of it drops during the failover.

cache TTL T     | provider share | app share     | outage (avg)  | outage (worst)    | dropped req
----------------|----------------|---------------|---------------|-------------------|-------------
            5 s |         32.4 s |         2.8 s |        35.2 s |            37.6 s |       49306
           15 s |         32.4 s |         7.8 s |        40.2 s |            47.3 s |       56306
           30 s |         32.4 s |        15.8 s |        48.2 s |            62.3 s |       67463
           60 s |         32.4 s |        29.9 s |        62.3 s |            90.4 s |       87150
          120 s |         32.4 s |        61.7 s |        94.1 s |           150.4 s |      131775

run-independent: provider share fixed at 32.4 s; as T goes from 5 to 120, outage rises 35.2 -> 94.1 s, dropped requests 49306 -> 131775.
the failover threshold is inaccessible, so detection time cannot be shortened; the only thing that can be shortened is T on the application side.

exit: 220 GB of work-order data, logical export at 40 MB/s, load at 26 MB/s (indexes are rebuilt).
time: export 1.56 h + load 2.41 h = 3.97 h. The snapshot cannot travel: the format sits in the inaccessible column.
destination      | egress fee (fictional unit/GB) | total (fictional unit)
-----------------|--------------------------------|------------------------
within region    |                              0 |                    0
cross-region     |                              2 |                  440
internet         |                              9 |                 1980
```

## The Inaccessible Column Is the Second-Largest Set

The delegated count is 8, or 28.6% of the catalog. This is the column the managed service
is selling, and what sits in it genuinely cuts workload: operating system patching, minor
version patching, backup format, and replication layout are now someone else's night
shift.

The unexpected part is the third column: **9 items, 32.1%**. These items were not
delegated; they vanished. There is no shell access, no data directory path, no direct
look at the log files. Some of these go unnoticed in their absence. Some are noticed the
day they are lost: the **failover threshold** sits in this column. How many seconds the
health check runs at and how many failures trigger a failover cannot be read or adjusted.
The same column holds the **write-ahead log threshold** and an **extension not on the
list**: if an extension the application needs is not on the provider's list, there is no
way to install it — in a database you run yourself, that is a package-install job.

The 11 kept items offer a misleading comfort. Four of them are **one-way**: a major
version upgrade cannot be undone, storage that has grown cannot shrink, the chosen
placement region does not change without moving the data, and the character-set-and-
collation choice is not fixed without rewriting tables. All four are dropdown menus on the
setup screen, and their consequences last the database's whole lifetime.

## What Gets Measured Is Not the Outage, but Who Owns It

Failover is the most talked-about feature of a managed service, and it is usually cited as
a single number. The table shows that single number is made of two parts.

**The provider's share is 32.4 seconds in all five rows.** Detection is 12.4 seconds (a
2.4-second phase plus two check intervals), the upgrade is 20 seconds. No setting changes
this number, because the item that produces it sits in the inaccessible column.

**The application's share climbs from 2.8 seconds to 61.7 seconds.** The whole difference
comes from the endpoint name's resolver-cache lifetime. At T = 5 seconds, the average
outage is 35.2 seconds and 49,306 requests drop; at T = 120 seconds, the average is 94.1
seconds and 131,775 requests drop. Same failover, same provider, 2.67 times the loss. The
worst-case column makes it even plainer: at T = 120, one application instance spends 150.4
seconds trying to connect to a dead address.

The rule that follows draws the boundary of the delegated decision: **the failover itself
is delegated, the moment it finishes inside the application is not.** The pool row points
at the same thing — when sixteen instances come back up, the new primary faces 1,920
connection requests at once; the connection limit is a kept item, and if it sits below
that number, the failover turns into an outage that outlasts the failover itself.

## The Exit's Cost Is the Format Being Locked

The last block applies the course's recurring item to this lesson. When data needs to move
somewhere else, the snapshot is useless: it is taken in the delegated column and becomes
unreadable in the inaccessible column. Only one path is left, **logical export**: 220 GB
of data comes out at 40 MB/s in 1.56 hours, and loads at the destination at 26 MB/s in
2.41 hours — loading is slower than reading because indexes are rebuilt. The total is 3.97
hours, plus an egress fee: 0 within a region, 440 across regions, 1,980 fictional units over the internet.

The ratio among these three numbers stays the same for the rest of the course. Data paid
no fee coming in; the same data got charged the moment it crossed a region boundary going
out. The time itself is also a cost: through those 3.97 hours, either two systems write in
parallel and the gap between them is closed by hand, or the work-order records freeze.
That is why placement region is counted among the one-way items — changing regions means
paying this four-hour window all over again.

## Summary

- Of 28 decisions, 8 were delegated to the provider, 11 stayed kept, and **9 became
  inaccessible**; that third set does not exist in a database you run yourself.
- Of the 11 kept items, 4 are one-way: the major version upgrade, storage growth,
  placement region, and collation choice are not reversed without rewriting the data.
- 32.4 seconds of the failover outage is the provider's fixed share; it cannot be
  shortened because the failover threshold is inaccessible.
- The application's share can climb from 2.8 to 61.7 seconds: within the same failover,
  dropped requests rise from 49,306 to 131,775, and the worst-case outage rises from 37.6
  to 150.4 seconds.
- Because the snapshot format is inaccessible, exiting forces a logical export: 3.97 hours
  for 220 GB, plus a 1,980 fictional units egress fee over the internet.

## Next Step

This lesson measured under the assumption of a single primary database: no other copy
could absorb the load at the moment of failover, the second machine only stood by. But
most of the work-order database's request mix is reads — meter-reading summaries, billing
queries, field-photo lookups. Opening a standby copy to reads spreads the load, but that
copy lags behind the primary. The next lesson scans that lag and counts how many reads
come back stale at each value; it then separates out requests with a **read-your-writes**
requirement, and measures how many requests routing them to the primary saves, and how
much it adds to the primary's load.
