Cloud and Managed Database Services Questions
Running databases as managed cloud services rather than self-hosting them: choosing and provisioning managed relational and NoSQL offerings (for example Amazon RDS/Aurora/DynamoDB, Azure SQL Database/Cosmos DB, Google Cloud SQL/Spanner), sizing instances and storage, and designing an architecture (read replicas, connection proxies and pooling for managed or serverless compute) to meet a latency or availability target. It covers comparing provisioned versus serverless/autoscaling pricing and operational models, forecasting capacity and cost, and migrating a database onto or between managed deployments: moving a self-hosted database to a managed service, switching a workload from provisioned to serverless (or back), changing a single-AZ deployment to multi-AZ, and the questions to ask before adopting a new managed vendor. The core question is the split of responsibility and cost between the cloud provider and the engineering team: what the provider takes on (patching, infrastructure-level HA, automated backups) versus what remains the team's job (configuration, capacity and cost tuning, security posture like encryption and key rotation). It does not cover backup and restore mechanics, RPO/RTO planning, replication or sharding internals, or SQL-level query diagnostics.
Describe how you would deploy a managed relational database for a production web application. Cover service selection (RDS/Aurora/Cloud SQL/Managed MySQL), sizing (instance class, storage type), read scaling, encryption, maintenance windows, and routine maintenance practices.
Sample Answer
Deploying a managed relational database for production is five deliberate decisions, not one "create instance" click: which service, what size, whether you need read scaling yet, encryption configured before first write, and a maintenance window you chose rather than accepted by default.
Service selection
- Amazon RDS: the broadest engine choice (MySQL, PostgreSQL, MariaDB, SQL Server, Oracle, Db2), engine-native replication, and the most flexible instance sizing including small burstable classes. The default choice for most web applications unless you have a specific reason to want more.
- Amazon Aurora: MySQL- and PostgreSQL-compatible, with storage-layer replication instead of engine-level replication, which gives materially faster failover (automatically switching to a healthy standby when the primary fails) and a higher write-throughput (the volume of write operations the database can sustain per second) ceiling on the largest instance classes. Worth the higher baseline cost when you expect to outgrow a single well-tuned RDS instance, or when failover speed matters more than for a typical web app.
- Cloud SQL: Google Cloud's equivalent to RDS for MySQL, PostgreSQL, and SQL Server, same category of decision as RDS on AWS.
For a typical production web application starting out, RDS or Cloud SQL on the engine your team already knows is the right default; reach for Aurora specifically when you can point to a concrete scaling reason, not by default.
Sizing: instance class and storage type
Pick the instance class from measured or realistically estimated CPU and memory need with headroom, not a round number picked for comfort. Storage type matters as much as instance class:
- gp3 (general purpose SSD): the right default for most online transaction processing (OLTP) workloads. It decouples provisioned IOPS (input/output operations per second: the count of individual read or write operations the volume can handle each second, distinct from throughput's raw bytes-per-second measure) and throughput from allocated storage size, so you are not forced to over-allocate storage just to get more IOPS.
- io2 / io2 Block Express: for latency-sensitive, very high-IOPS workloads where gp3's ceiling is not enough; costs more per provisioned IOPS.
- Enable storage autoscaling so unplanned data growth raises allocated storage automatically instead of the instance running out of space and rejecting writes.
Read scaling
Add a read replica only when there is a specific, measured reason: reporting or analytics queries competing with transactional traffic for CPU and I/O, or read throughput that a single writer genuinely cannot serve even with caching in front of it. A read replica is a full additional instance (real cost, real operational surface via replication lag, the delay before a write on the primary becomes visible on the replica), not a free performance knob, so route only the traffic that needs it, typically through RDS Proxy or explicit read/write splitting in the application, rather than defaulting every read query to a replica.
Encryption
Enable storage encryption at creation time: RDS cannot add encryption to an existing unencrypted instance in place, only by snapshotting it, copying the snapshot with encryption enabled, and restoring from that copy, so getting this right on day one avoids a disruptive migration later. The AWS-managed key (which AWS automatically rotates every year) is sufficient for most applications; use a customer-managed KMS (Key Management Service) key only when you need explicit control over key policy or rotation cadence, for example a compliance requirement to control who can disable access to the key. Separately, enforce TLS (transport layer security) on connections at the engine level, rds.force_ssl for PostgreSQL or require_secure_transport for MySQL, since encryption at rest alone does not protect credentials and query data in flight between the application and the database.
Maintenance windows and routine maintenance
If you do not set a maintenance window explicitly, RDS assigns a random 30-minute window in your region, which may land during business hours. Choose one deliberately, during genuinely low-traffic hours for your actual user base, and prefer applying non-urgent changes during that window rather than immediately, so patches land predictably instead of mid-peak-traffic. Keep automatic minor-version upgrades on for security patches, but review major version upgrades manually since those can carry breaking changes to the engine's behavior. As routine practice: review the instance's "pending maintenance actions" on a regular cadence rather than being surprised by them, audit the parameter group for drift from intended settings after any manual tuning, and revisit instance rightsizing on a schedule (for example quarterly) instead of only reacting once performance has already degraded.
Worked example: a concrete creation command
aws rds create-db-instance \
--db-instance-identifier prod-webapp-db \
--engine postgres --engine-version 16.4 \
--db-instance-class db.r6g.xlarge \
--allocated-storage 200 --storage-type gp3 --iops 6000 --storage-throughput 250 \
--max-allocated-storage 500 \
--storage-encrypted --kms-key-id alias/prod-db-key \
--preferred-maintenance-window "sun:05:00-sun:06:00" \
--auto-minor-version-upgrade
This encodes the sizing decision (an r6g.xlarge, memory-optimized Graviton instance class, a common default for a moderate production web app), gp3 storage with explicit IOPS and throughput rather than whatever the default happens to be, storage autoscaling up to 500 GiB so growth does not hit a hard wall, encryption specified at creation with a named key, a chosen (not default) maintenance window, and automatic minor-version patching. Multi-AZ and backup retention are deliberately left off this example: they are availability and disaster-recovery decisions with their own trade-offs, not part of the base provisioning choice this question is about.
Trade-offs and pitfalls
- Defaulting to the largest instance class "to be safe" wastes money every hour it runs oversized; defaulting to the smallest "to save money" invites a production incident the first time traffic grows. Size from data, and revisit it, rather than picking once and forgetting.
- Adding a read replica before you have a measured reason adds real operational surface (monitoring replica lag, routing logic) for no benefit, and it is easy to add one reflexively because "that's what scaling a database looks like."
- Forgetting to set storage encryption before the first write is the single most expensive mistake in this list to fix later, since fixing it means a snapshot-copy-restore cycle on a live database rather than a config change.
- Accepting the default maintenance window is a common, easily avoided source of an unplanned mid-day blip; it costs nothing to set deliberately at creation time.
Explain connection pooling and why it matters for managed databases, especially with serverless compute (AWS Lambda) and managed MySQL/Postgres. Name pooling solutions you'd consider, explain how you'd size the pool, and describe what you'd configure to avoid connection storms.
Sample Answer
Connection pooling reuses a small set of already-open database connections across many requests instead of opening a fresh connection (a TCP handshake plus authentication) for every request. It matters most acutely with serverless compute like AWS Lambda because each concurrent Lambda execution environment behaves like an independent short-lived process: if your code opens a database connection on invocation, a burst of concurrent invocations opens that many connections nearly simultaneously, and a managed MySQL or PostgreSQL instance has a hard max_connections ceiling tied to its instance size. A traffic spike that is entirely reasonable in query volume can still take the database down purely on connection count.
Why it matters beyond Lambda
Every open connection costs the database real memory and, for PostgreSQL specifically, a whole backend OS process, so connections are expensive per-unit even when idle. Opening and closing them constantly (rather than reusing a pool) adds latency and CPU overhead from repeated authentication and TLS negotiation on every request, on top of the risk of simply running out of connection slots.
Pooling solutions to consider
- RDS Proxy: AWS-managed, sits between the application and RDS (Amazon's managed relational database service for engines like MySQL and PostgreSQL) or Aurora (AWS's own MySQL- and PostgreSQL-compatible managed database with a different underlying storage engine). It pools and multiplexes connections, supports IAM authentication and credentials via AWS Secrets Manager, and automatically fails over to a standby while preserving application connections. It can only be attached to a writer instance, not a read replica, and it requires the proxy and the database to be in the same virtual private cloud (VPC).
- PgBouncer: self-run, the standard external pooler for PostgreSQL, used constantly in front of Lambda-to-Postgres workloads because of its transaction-mode pooling.
- ProxySQL: the MySQL equivalent, adding query routing and caching on top of pooling.
- Cloud SQL Auth Proxy / connectors: Google Cloud's equivalent pattern for Cloud SQL, handling authenticated, pooled connections from serverless callers.
Sizing the pool
Size the pool from actual concurrency, not guesswork, using Little's Law: the average number of concurrent database connections a workload needs equals its request rate multiplied by the average time each request holds a connection.
L=λ×Wrequests_per_sec = 2000 # peak invocation rate hitting the database
avg_query_duration_s = 0.015 # 15ms average time from "acquire connection" to "commit/release"
concurrent_db_connections_needed = requests_per_sec * avg_query_duration_s
db_max_connections = 1000 # example ceiling for a large Aurora instance class
usable_max_connections = db_max_connections - 50 # reserve headroom for admin/replication
headroom_multiplier = 3 # slack for variance and short bursts above the mean
recommended_pool_size = concurrent_db_connections_needed * headroom_multiplier
print(f"Concurrent DB connections needed on average: {concurrent_db_connections_needed:.1f}")
print(f"Recommended pool size with {headroom_multiplier}x headroom: {recommended_pool_size:.0f}")
print(f"That is {recommended_pool_size/usable_max_connections:.1%} of usable max_connections")
Concurrent DB connections needed on average: 30.0
Recommended pool size with 3x headroom: 90
That is 9.5% of usable max_connections
The point this makes concrete: even at 2,000 requests/sec, if each request only holds a connection for 15ms, the database only ever needs about 30 connections open at once on average, 90 with generous headroom. A pooler lets a huge number of Lambda-side clients share that small, stable set of database-side connections, which is the entire reason pooling and Lambda go together.
What to configure to avoid connection storms
- Use transaction-mode pooling, not session mode. In transaction mode, the database-side connection is returned to the pool the instant a transaction commits, not held for the life of the client's connection to the pooler. This is what lets 30 to 90 real database connections serve thousands of Lambda invocations: each invocation borrows a connection for milliseconds, not for its entire lifetime.
- Point Lambda only at the pooler's endpoint, never at the database engine directly. If any code path bypasses the pooler, it reintroduces the exact one-connection-per-invocation problem pooling exists to solve.
- Set the pooler's client-facing limit high but its database-facing limit bounded near the database's real capacity: thousands of client-side slots, tens to low hundreds of database-side ones.
- Set idle and statement timeouts so an abandoned or slow Lambda-side connection cannot hold a pooled database slot indefinitely.
- Avoid patterns that force "pinning." RDS Proxy (and poolers generally) fall back to a dedicated, unshared connection for a session that changes connection-level state, for example a session-level
SETstatement, an unclosed transaction, or, on RDS Proxy specifically, any statement larger than 16 KB. A pinned session stops multiplexing entirely for its lifetime, which quietly reintroduces the connection-storm risk the pooler was meant to prevent.
Trade-offs and pitfalls
- A pooler is another moving part with its own failure modes: if it goes down, so does every connection through it, so it needs to be highly available in its own right (RDS Proxy handles this for you; a self-run PgBouncer needs its own redundancy plan).
- Transaction-mode pooling breaks session-level features that assume a stable connection: prepared statements that persist across queries, session-level temp tables, and
LISTEN/NOTIFYin Postgres either don't work or behave differently. Audit the application for these before switching modes. - Pooling fixes connection exhaustion; it does not fix a database that is genuinely CPU- or IO-bound. If queries are slow, a pool just queues requests instead of rejecting them outright, which can turn a fast failure into a slow, harder-to-diagnose one if queue depth isn't monitored.
Evaluate the trade-offs of using a managed multi-region database service (for example Cloud Spanner, Cosmos DB, or Aurora Global Database) versus running a self-managed sharded cluster on VMs or Kubernetes, for a system handling roughly 10 million QPS reads and 200,000 writes per second. Cover latency, consistency, operational complexity, cost, scaling behavior, and disaster-recovery capabilities, and give your recommendation.
Sample Answer
Direct answer
At 10 million reads per second and 200,000 writes per second, the default recommendation is a managed multi-region service (Spanner-class horizontal partitioning or Cosmos DB-class tunable consistency) over a self-managed sharded cluster, unless the team already operates comparable distributed stateful infrastructure in-house and the consistency requirements are loose enough to accept asynchronous cross-region replication. At this scale the operational surface a managed vendor absorbs, quorum health (whether enough replicas are up and agreeing to keep accepting writes), rebalancing, and cross-region failover (automatically switching traffic to a healthy region if one goes down) across what turns out to be hundreds of nodes, is exactly the class of problem these services are purpose-built for. The worked example and framework below show why, and name what would flip the recommendation.
Framework: six axes
| Axis | Managed multi-region (for example Cloud Spanner, Cosmos DB, Aurora Global Database) | Self-managed sharded cluster (VMs or Kubernetes) |
|---|---|---|
| Latency | In-region reads and writes stay low: Cosmos DB's own documentation guarantees a 99th-percentile (P99) latency under 10 ms for reads and writes within a region, at any consistency level. Cross-region strong consistency is bounded by physics, not engineering: Cosmos DB states cross-region strong-consistency write latency as two times the round-trip time (RTT) between the two farthest regions, plus 10 ms P99. | Fully tunable: keep the write quorum inside one metro region for low latency, and replicate elsewhere asynchronously for disaster recovery (DR) only, if the business can accept that. Requires the team to design and prove this topology correct itself. |
| Consistency | Comes largely built in. Cosmos DB offers five levels (Strong, Bounded Staleness, Session, Consistent Prefix, Eventual). Spanner gives a single strong guarantee, external consistency (global linearizability, meaning all observers agree on the order transactions actually committed in), via TrueTime, a globally synchronized clock with a bounded uncertainty window that every commit waits out before acknowledging. | Owned entirely by the team. Single-shard transactions are ordinary single-node ACID (atomicity, consistency, isolation, durability). Cross-shard transactions need a distributed-transaction layer, for example two-phase commit (a protocol that coordinates an atomic commit across multiple shards), that the team designs, implements, and must prove correct under partition and retry. |
| Operational complexity | Patching, quorum membership, internal rebalancing, and backup mechanics are the vendor's job. The team owns schema design, indexing, provisioned or consumed throughput sizing, and query-level tuning. | Everything: failover automation, connection pooling and proxy layers, monitoring, and on-call runbooks for partition and quorum-loss scenarios are the team's to write and operate, with no vendor site reliability engineering (SRE) team absorbing pages. |
| Cost | Priced at a premium over commodity compute (consumption-based request units, or per-node or per-vCPU list pricing depending on the specific service and tier; check the current pricing page for the exact service and region before budgeting, since list prices change without a fixed schedule). The premium buys engineering time the team does not have to spend building the same capability. | Commodity VM or Kubernetes compute, storage, and network egress rates, but the true cost includes the engineering headcount to build and operate the sharding, failover, and DR tooling a managed service ships by default. That cost does not appear on the cloud bill. |
| Scaling behavior | Elastic and mostly automatic. Aurora Global Database adds up to 10 read-only secondary regions with storage-based replication typically under a second of lag, though only the primary region accepts direct writes (a "write forwarding" feature lets secondaries relay writes to the primary, at the cost of that round trip). Spanner and Cosmos DB partition writes horizontally across internal shards transparently to the application. | Fully within the team's control (shard key choice, resharding strategy, instance sizing), but every reshard is a project the team runs and risks itself, exactly the work automatic rebalancing would otherwise absorb. |
| Disaster recovery | Documented, tested guarantees. A Cloud Spanner multi-region configuration places two read-write regions (two replicas each) plus a witness region (a fifth voting replica that participates in the write quorum but does not serve reads), five voting replicas total; a write quorum needs the leader region's replica plus any two of the other four, so the configuration survives the complete loss of any one region with zero data loss, a recovery point objective (RPO) of zero. Aurora Global Database offers a planned switchover (no data loss) and an unplanned failover after a regional outage (a small, non-zero RPO bounded by the sub-second replication lag (the delay between a write completing in the primary region and arriving in the secondary) at the moment of failure). | Fully bespoke: cross-region async replication, backup and restore, or a warm standby are all buildable, but the recovery time objective (RTO) and RPO the system actually delivers are only as good as the team's own runbook and the disaster tests actually run, not a documented product guarantee. |
Worked example
Whichever path is chosen, the raw infrastructure needed at this scale is a useful reality check: 10 million reads/sec and 200,000 writes/sec is real online transaction processing (OLTP) hardware regardless of who operates it.
import math
# Inputs: the QPS figures come from the question; the rest are stated,
# illustrative capacity-planning assumptions (NOT vendor-measured benchmarks).
read_qps = 10_000_000
write_qps = 200_000
read_capacity_per_node = 50_000 # assumed sustained cached-read throughput of one well-provisioned OLTP node
write_capacity_per_shard = 8_000 # assumed sustained write throughput of one shard's single-writer primary
dr_regions = 3 # regions in the deployment
shards_needed = math.ceil(write_qps / write_capacity_per_shard)
total_read_nodes = math.ceil(read_qps / read_capacity_per_node)
read_nodes_per_shard = math.ceil(total_read_nodes / shards_needed)
nodes_per_shard_group = 1 + read_nodes_per_shard # 1 primary + its read nodes
total_nodes_single_region = shards_needed * nodes_per_shard_group
total_nodes_all_regions = total_nodes_single_region * dr_regions
print(f"shards needed (write-bound): {write_qps:,} / {write_capacity_per_shard:,} -> {shards_needed}")
print(f"read-serving nodes needed (read-bound): {read_qps:,} / {read_capacity_per_node:,} -> {total_read_nodes}")
print(f"read nodes per shard: {total_read_nodes} / {shards_needed} -> {read_nodes_per_shard}")
print(f"nodes per shard group (1 primary + reads): {nodes_per_shard_group}")
print(f"total nodes, single region: {shards_needed} x {nodes_per_shard_group} -> {total_nodes_single_region}")
print(f"total nodes, {dr_regions}-region deployment: {total_nodes_single_region} x {dr_regions} -> {total_nodes_all_regions}")
Output:
shards needed (write-bound): 200,000 / 8,000 -> 25
read-serving nodes needed (read-bound): 10,000,000 / 50,000 -> 200
read nodes per shard: 200 / 25 -> 8
nodes per shard group (1 primary + reads): 9
total nodes, single region: 25 x 9 -> 225
total nodes, 3-region deployment: 225 x 3 -> 675
Roughly 675 stateful nodes across three regions, on these stated assumptions. That number does not change based on who manages it: a managed multi-region service still provisions and rebalances something in that range internally, it simply does not page the team when a node in it fails. The real question is whether the team wants to own the failure domain for 675 stateful nodes directly, given that a self-managed cluster at this size effectively requires building an internal SRE function for exactly that fleet.
flowchart LR
subgraph MG["Managed multi-region service"]
C1["Client"] --> R1["Regional endpoint"]
R1 --> P1[("Primary region")]
P1 -. replicates .-> S1[("Secondary region A")]
P1 -. replicates .-> S2[("Secondary region B")]
end
subgraph SM["Self-managed sharded cluster"]
C2["Client"] --> RT["Shard router"]
RT --> SH1[("Shard 1 primary")]
RT --> SH2[("Shard 2 primary")]
SH1 --> RE1[("Shard 1 replica")]
SH2 --> RE2[("Shard 2 replica")]
end
In the managed topology, the vendor owns everything to the right of the regional endpoint: which replica leads, how replication and quorum work, how a lost region is detected and routed around. In the self-managed topology, the team owns and must operate the shard router, the primary and replica wiring per shard, and the logic for what happens when a shard's primary disappears.
Recommendation and what would flip it
Recommend the managed multi-region path by default at this scale. The deciding factors are the team's existing distributed-systems operational maturity (does it already run something like Vitess- or CockroachDB-class infrastructure at hundreds of nodes today), the actual consistency requirement (does the product need synchronous cross-region strong consistency, or would single-writer-per-shard with asynchronous DR replication genuinely be acceptable), and whether the cost delta at roughly 675 nodes' worth of infrastructure materially matters to the budget, since the managed premium multiplies across a large fleet at this scale rather than a handful of instances.
It flips to self-managed when all three line up: the team already operates comparable stateful infrastructure in-house, the workload tolerates a single-writer-per-shard model with asynchronous cross-region replication instead of synchronous strong consistency, and the cost savings at this node count are large enough to justify paying the engineering-time cost once, building the sharding, failover, and DR tooling, instead of paying the managed-service premium every month indefinitely. Defaulting to "managed is always safer" ignores that at 675 nodes the managed premium is not a rounding error, and a team that already has the operational muscle for a fleet this size may reasonably prefer to keep that premium as engineering budget instead.
Trade-offs and pitfalls
The dominant failure mode on this kind of question is picking a side without naming the deciding variable, or hedging into "it depends" without ever committing. A second common mistake is assuming managed multi-region services give multi-region strong consistency for free: Cosmos DB explicitly does not support strong consistency on a multi-region-write account, and even Spanner's external consistency costs real latency, a bounded commit-wait on every write, it is not free ordering. A third is treating "self-managed" as automatically cheaper: at 675 nodes, the engineering cost of building and operating correct distributed-transaction and failover logic in-house is usually larger, not smaller, than the managed premium, unless that capability already exists in the organization.
Describe the step-by-step plan to migrate a production MySQL database from self-hosted VMs to Amazon RDS with minimal downtime. Include schema transfer, initial data load, how you'd keep the two in sync until cutover, migrating users and permissions, cutover strategy, smoke tests, and rollback plan.
Sample Answer
The shape of a minimal-downtime migration to a managed service is always the same regardless of engine: get the schema and a data snapshot onto the target first, then keep the target continuously in sync with the source via replication while the application keeps running against the source, then do a short, controlled cutover that only has to cover the last few seconds of lag, not the whole dataset.
Step-by-step plan (MySQL self-hosted to Amazon RDS for MySQL)
1. Schema transfer. Dump the schema only (mysqldump --no-data) and review it for anything RDS restricts, for example storage engines other than InnoDB, filesystem-level FILE privilege usage, or custom plugins, since RDS does not give you host access to install them. Apply the reviewed schema to the new RDS instance.
2. Capture a consistent starting point and load the data. On the source, briefly make it read-only and record the exact binary log (binlog) position writes will resume from:
FLUSH TABLES WITH READ LOCK;
SET GLOBAL read_only = ON;
SHOW MASTER STATUS;
-- File: mysql-bin-changelog.000031, Position: 107
Check the source's MySQL version before you run this: MySQL 8.4 removed SHOW MASTER STATUS entirely (tested against a real MySQL 8.4.11 instance: it fails with ERROR 1064: You have an error in your SQL syntax), replacing it with SHOW BINARY LOG STATUS, which returns the identical File / Position columns. Use SHOW MASTER STATUS for a MariaDB or MySQL 8.0-and-earlier source, SHOW BINARY LOG STATUS for a MySQL 8.4+ source. Since a self-hosted fleet migrating today is plausibly already on 8.4, confirm with SELECT VERSION(); first rather than assuming the older syntax works.
Then load the data into RDS with a consistent, compressed dump and immediately unlock the source (this lock window is seconds, not the length of the whole migration):
mysqldump --databases mydb --single-transaction --compress --order-by-primary \
-u local_user -plocal_password | mysql --host=mydb.xxxxx.us-east-1.rds.amazonaws.com \
--port=3306 -u rds_user -prds_password
SET GLOBAL read_only = OFF;
UNLOCK TABLES;
--single-transaction avoids a long table lock on InnoDB tables by taking a consistent snapshot inside one transaction, which is what keeps this step from blocking the live application.
3. Keep the two in sync until cutover. Create a replication user on the source with the minimum privileges replication needs, then tell RDS to become a live replica of the source starting exactly at the binlog position captured in step 2:
-- on the source
CREATE USER 'repl_user'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'repl_user'@'%';
-- on the RDS instance, as the master user (MySQL 8.4+ syntax; use rds_set_external_master
-- on MariaDB and MySQL 8.0 and earlier)
CALL mysql.rds_set_external_source('source.mydomain.com', 3306, 'repl_user', 'password',
'mysql-bin-changelog.000031', 107, 0);
CALL mysql.rds_start_replication;
RDS now streams every write the source makes from that point forward. The source's security group or firewall has to allow inbound connections from RDS's IP, and the source keeps serving the live application unchanged during this whole window, which can run for hours or days if needed.
4. Migrate users and permissions. RDS does not expose the mysql.user table for direct copying (there's no host-level MySQL account on a managed instance), so recreate accounts explicitly: script out every CREATE USER and GRANT statement from the source (SHOW GRANTS FOR user@host per account) and replay them against RDS, adjusting anything that depended on filesystem or SUPER privileges the RDS master user does not have.
5. Cutover. Watch replication lag (SHOW REPLICA STATUS / the Seconds_Behind_Source field, or the RDS ReplicaLag CloudWatch metric) until it is at or near zero. Put the application into a brief maintenance/read-only mode, wait for lag to hit zero (this should now be a few seconds, not the original full migration time), stop replication (CALL mysql.rds_stop_replication), repoint the application's connection string to the RDS endpoint, and take the application out of maintenance mode.
6. Smoke tests. Before declaring success: run row counts and CHECKSUM TABLE on the highest-value tables on both source and target and confirm they match exactly; run the application's own smoke test suite (login, a representative write, a representative read) against the RDS endpoint; confirm the replication user and any migration-only grants are revoked.
7. Rollback plan. Do not decommission the source. Keep it running, unmodified, for a defined window after cutover (for example 24 to 72 hours) so that if something is wrong with the RDS target, traffic can be pointed back to the source immediately with no data loss, since the source was only ever paused for the seconds-long lock in step 2, never stopped. This rollback window is only clean while the source has not diverged: once the application is writing to RDS post-cutover, "rolling back" means accepting that writes made after cutover are on RDS and would need to be manually reconciled back to the source, so treat the rollback option as valid within the cutover maintenance window and increasingly costly the longer you wait after it.
Postgres variant: logical replication instead of binlog shipping
For a single-node PostgreSQL source, the equivalent sync mechanism is native logical replication rather than binlog shipping, and RDS for PostgreSQL supports it directly. On RDS, an account with the rds_superuser and rds_replication roles (the two elevated permission roles RDS grants in place of true Postgres superuser access; this one needs both to turn logical replication on) sets the static parameter rds.logical_replication = 1 (this also raises wal_level, how much detail Postgres records in its write-ahead log, from just enough for crash recovery up to enough to support logical replication, and max_wal_senders, the number of concurrent replication connections the source can serve, and related parameters) and reboots the instance to apply it. On the source:
CREATE PUBLICATION mig_pub FOR ALL TABLES;
On the RDS target:
CREATE SUBSCRIPTION mig_sub
CONNECTION 'host=source.mydomain.com port=5432 dbname=mydb user=repl_user password=secret'
PUBLICATION mig_pub;
Unlike the MySQL flow, this single CREATE SUBSCRIPTION statement does both the initial data copy and then keeps streaming ongoing changes, so there's no separate "load the dump, then start replication" step. The two things to check before committing to this path that MySQL doesn't force on you: extension compatibility (RDS PostgreSQL only supports a curated allow-list of extensions, so SELECT * FROM pg_available_extensions on the RDS target against what the source actually uses, before migrating, not after) and that every table has a primary key or unique index (logical replication can't apply UPDATE/DELETE without one). For a smaller database that can tolerate a real maintenance window instead of a live sync, pg_dump/pg_restore with --jobs for parallelism is simpler and avoids the replication setup entirely; it trades a longer, bounded downtime window for one less moving part.
Post-cutover verification (both engines)
Row counts alone can hide corruption (same count, wrong rows), so verify per table with a cheap content check, not just COUNT(*):
-- MySQL
CHECKSUM TABLE orders, order_items, users;
-- PostgreSQL
SELECT count(*), sum(hashtext(t::text)) FROM orders t;
Run the same query on source and target and diff the results before you call cutover done. A mismatch here, caught before the maintenance window closes, is recoverable; the same mismatch discovered a week later, after both databases have diverged, is not.
Think of a managed database service you have personally deployed and operated in production (for example RDS, Aurora, Cloud SQL, DynamoDB, or Cosmos DB). Walk through how you approached capacity planning and your scaling strategy for it, and share one concrete outcome or lesson learned from running it in production.
Sample Answer
This question is scored on specificity, not on which service gets named. A strong answer names the actual signal that triggered a capacity decision (a real metric and threshold, not "we noticed it was slow"), the actual scaling mechanism chosen and why that one over the alternatives, and one concrete lesson that changed how the system was operated afterward, a real before-and-after in process, not a restated best practice.
What a strong answer actually contains
- A specific capacity signal, not a vague symptom. "We watched CPU" is not a signal; "the burst credit balance on a burstable instance class was heading toward zero every afternoon" is, because it names the actual metric, the actual pattern, and why that pattern meant trouble was coming rather than already fine. (A burstable instance class, for example AWS's T-family, earns credits while it runs below its baseline CPU level and spends those credits to run faster than that baseline when needed; once the credit balance reaches zero, performance drops back down to the lower baseline, which is the actual mechanism that turns a shrinking balance into a real, user-facing slowdown.)
- The scaling mechanism chosen, and why it beat the obvious alternative. Interviewers are listening for the reasoning that ruled out the simpler option, not just the option that was picked.
- One concrete lesson framed as a process change, something that is genuinely different about how the system, or the team, operates now versus before the incident, not a generic takeaway that would apply to any database anywhere.
What undermines this answer
A memorized capacity-planning checklist with no specific metric, no specific trigger, and no real outcome attached reads as rehearsed rather than experienced, which is precisely what this question is designed to surface. Naming an impressive-sounding managed service is not the point; the reasoning and the actual outcome are.
Illustrative example of the shape a strong answer takes
A production RDS (Amazon Relational Database Service) MySQL instance on a burstable instance class (db.t3) ran fine most of the day, but a daily afternoon analytics batch job steadily depleted its CPU credit balance, and by the third month, users started reporting slow page loads that correlated with when that job ran. The real signal was the CPUCreditBalance CloudWatch metric (the metric AWS documents specifically for T-family instance classes, distinct from BurstBalance, which tracks gp2 storage I/O credits and applies regardless of instance class) heading toward zero every afternoon, not flatlined at zero, which meant the instance was surviving that day by borrowing against a shrinking buffer, a materially different situation than simply being fine. The first instinct was to move the batch job to overnight, but that job fed a report a stakeholder needed by mid-afternoon, so the timing was not actually negotiable. The real fix was migrating that instance off the burstable instance class onto a non-burstable class sized for the batch job's sustained CPU demand, not just its average load, since a T-family instance's baseline CPU only refills credits while running below that baseline, and a daily job that regularly pushes CPU above baseline for long enough will eventually always run the balance to zero no matter how the alarm is tuned. The lesson that changed how the team operated afterward: CPU-credit alarms got set at 50% remaining, not a default-feeling 10%, because by the time the balance actually hit 10% there were maybe two hours of runway left before it became a customer-facing incident, nowhere near enough lead time to safely plan and execute an instance-class change on a production database. That alarm-threshold change, not the specific database engine involved, is the actual takeaway a strong answer should be able to name concretely.
Trade-offs and pitfalls
- A "lesson learned" that is really a restated best practice ("always monitor your database") without a specific behavior change attached does not clear the bar this question sets; the question explicitly asks what changed, not what is generally good practice.
- Candidates sometimes reach for the most technically impressive incident they can recall rather than the one they can describe with the most real detail; a smaller, well-specified incident beats a large, vaguely-remembered one here.
- The strongest answers make clear which part was a genuine surprise at the time (the credit balance pattern, in the example above) versus which part was a deliberate, reasoned trade-off (ruling out simply rescheduling the job); collapsing those two into one flat narrative loses the signal an interviewer is actually listening for.
Unlock Full Question Bank
Get access to all 20 Cloud and Managed Database Services interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.