log in
consulting hosting industries the daily tools about contact

The Real Math on RDS vs. a Tuned MariaDB VM

Everyone says 'just use RDS.' I run managed hosting and I've done the math. Here's what it actually costs you.

RDS is the right answer for a lot of teams. It's not the right answer for mine, and I suspect it's not the right answer for a lot of small hosting shops either. Here's the math I did, and why I keep landing on a well-tuned MariaDB VM for most of my managed clients.

The Setup

NWOS runs managed hosting for somewhere around 30 client applications — Laravel apps, mostly, across healthcare, e-commerce, real estate, and a few specialty manufacturing clients. These aren't toy projects. They have real traffic, real data, and real SLAs. When I set up managed hosting for a new client, I have to make a decision about the database tier, and that decision has real dollar consequences.

For the last few years I've been running a dedicated VM per client or a shared high-memory VM with isolated MariaDB instances, depending on the client's load profile. The alternative I keep pricing out is RDS — either MySQL-compatible or Aurora. Every time I do the math, I end up back at the VM.

Let me show you why.

The Actual Numbers

Let's take a concrete mid-tier client: a regional e-commerce shop doing maybe 50k orders a year, a few hundred concurrent users at peak, a database sitting at around 40GB of actual data. Not a huge workload. Not trivial either.

RDS option: db.t4g.medium, Multi-AZ, MySQL 8.0

  • Instance (on-demand, Multi-AZ): ~$120/mo
  • Storage (100GB gp3, Multi-AZ doubles the I/O cost): ~$25/mo
  • Backup storage (let's say 7 days, 100GB): ~$10/mo
  • Data transfer out (modest, 50GB/mo): ~$4.50/mo
  • Total: ~$160/mo

Okay, that's not crazy. But that's for one client. Now let's add a second client with a similar profile. You're at $320/mo and you have two separate RDS instances to manage, monitor, and pay for separately. The costs are additive with almost no economies of scale until you're spending enough to justify Reserved Instances, which locks in capital for 1-3 years.

Also — and this is important — that db.t4g.medium is a burstable instance. It has 2 vCPUs and 4GB of RAM. For a lot of workloads that's fine until it isn't, and when it isn't, you're looking at db.r6g.large territory at $220/mo just for the instance, before Multi-AZ, before storage, before anything.

The VM option: a dedicated $200/mo Hetzner or Vultr dedicated/bare-metal instance

I've been running a Hetzner AX41-NVMe for certain clients — 4 cores, 64GB RAM, 2x512GB NVMe in software RAID-1. That's $65/mo. For the slightly heavier option I use their AX52 — 8 cores, 128GB RAM — at around $110/mo.

But let's say I'm on a cloud provider to keep things comparable. A Vultr or DigitalOcean dedicated CPU instance with 8 vCPUs and 32GB RAM runs about $200/mo. That's the number I'm going to use.

On that $200/mo box, I can run:

  • 5-8 isolated MariaDB instances for different clients (using separate OS users, separate data directories, separate my.cnf configs)
  • Proper tuning per client workload
  • Backups via a mix of MariaDB's native mariabackup, periodic mysqldump to object storage, and optional binary log shipping
  • Monitoring via Prometheus + mysqld_exporter + Grafana, which I already run for everything else

Per client, that $200/mo box works out to $25-40/mo for the database tier. Compare that to $160+/mo per client on RDS.

The Tuning That Makes It Work

The reason this math works is that a default MariaDB install is not what you're running. You tune it. Here's the actual my.cnf section I start with for a production Laravel app on a shared 32GB RAM server, where I'm budgeting about 6GB for this particular instance:

[mysqld]
# InnoDB buffer pool — the single most important knob.
# Set to ~70-75% of the RAM you're allocating to this instance.
innodb_buffer_pool_size         = 4G
innodb_buffer_pool_instances    = 4

# Redo log — bigger means less frequent checkpoints, better write throughput.
innodb_log_file_size            = 512M
innodb_log_buffer_size          = 64M

# Flush behavior — O_DIRECT skips double buffering with the OS page cache.
innodb_flush_method             = O_DIRECT

# Don't flush on every commit if you can afford to lose ~1 second of transactions.
# For most web apps this is fine. For payment ledgers, keep it at 1.
innodb_flush_log_at_trx_commit  = 2

# Connections — don't over-provision. Laravel uses a connection pool anyway.
max_connections                 = 150
thread_cache_size               = 50

# Query cache is deprecated in MySQL 8 but still in older MariaDB.
# Leave it off. It's a global mutex waiting to hurt you.
query_cache_type                = 0
query_cache_size                = 0

# Temp tables — spill to disk less often.
tmp_table_size                  = 64M
max_heap_table_size             = 64M

# Slow query log — always on in production. You want this data.
slow_query_log                  = 1
slow_query_log_file             = /var/log/mysql/slow.log
long_query_time                 = 1
log_queries_not_using_indexes   = 0

# Binary logging for point-in-time recovery.
log_bin                         = /var/log/mysql/mysql-bin
binlog_format                   = ROW
expire_logs_days                = 7

# Character set — always explicit, always utf8mb4.
character_set_server            = utf8mb4
collation_server                = utf8mb4_unicode_ci

That config alone turns a mediocre MariaDB install into something that handles real traffic without breaking a sweat. I've run Laravel apps doing 300+ requests/second against instances tuned like this without seeing the DB become the bottleneck.

The Laravel .env side is also worth noting:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3309
# Note the non-standard port — each client instance gets its own port.
# This is how I isolate multi-tenant instances on the same box.
DB_DATABASE=client_production
DB_USERNAME=client_app_user
DB_PASSWORD=
DB_OPTIONS_CHARSET=utf8mb4

Each client gets a different port (3306, 3307, 3308...), their own OS-level MySQL user and data directory, their own my.cnf, and their own backup schedule. It's not magic. It's just basic Unix hygiene.

What RDS Actually Buys You

I want to be fair here. RDS is not a bad product. Here's what you genuinely get that I don't get out of the box on a VM:

Automated failover. Multi-AZ RDS will fail over in 60-120 seconds with no manual intervention. My VM setup requires either a floating IP + Keepalived + a replica you promote by hand, or something like ProxySQL in front of two instances. That's real operational work.

Read replica setup is trivial. Click a button, you have a read replica. On a VM, I'm writing replication configs, managing GTID state, and monitoring replica lag myself.

IAM-based auth and at-rest encryption are easy to enable. Not impossible to do on a VM, but RDS makes it a checkbox.

Managed minor version patching. This is actually underrated. When a CVE drops for MySQL, AWS patches it. I have to stay on top of that manually.

For teams without a dedicated ops person — a startup with two engineers shipping product — RDS is probably the right call. The economics I'm describing only work if someone (me, in this case) is competent to run the underlying infrastructure and doesn't mind doing so.

When I'd Reach for RDS

  • A client with compliance requirements (HIPAA, PCI) where the audit trail on managed services is genuinely valuable
  • A startup that can't afford to care about this layer and needs to move fast
  • A workload with genuinely unpredictable spikes where Aurora Serverless v2's auto-scaling is worth the premium
  • Any situation where I'm being asked to run a single database for a single client and the VM economies of scale don't apply

When I Wouldn't

  • Any managed hosting context where I'm running multiple client databases and can amortize the VM cost
  • Workloads where I have a clear read/write profile and tuning will actually help
  • Clients on a tight hosting budget where $120/mo for a db.t4g.medium is real money
  • Anywhere I'm already running other infrastructure on the same box (a Redis instance, a queue worker, a cron server) and the marginal cost of adding a DB is nearly zero

The Part Nobody Talks About

The hidden cost of RDS isn't the compute. It's the data transfer and the storage IOPS. Every time your app pulls a 10MB export, that egress adds up. Storage on RDS is provisioned IOPS or gp3 with limits — and if your client does a lot of bulk inserts or analytics queries, you will notice the I/O ceiling on a t-class instance. On my NVMe-backed VM, I/O is effectively unlimited for any web workload I've thrown at it.

I had a biotech client a couple years ago running an RDS db.t3.medium that was CPU-throttled constantly — burning through its burst credits by 10 AM every day. Migrating them to a self-managed MariaDB on a $60/mo Hetzner box with proper InnoDB tuning dropped their query latency by 60% and their hosting bill by $80/mo. The server never breaks a sweat.

The Bottom Line

If you're running a hosting business or managing infrastructure for multiple clients, RDS's per-instance pricing will eat you alive compared to a well-run VM. The operational overhead is real but manageable if you know what you're doing. Do the math for your actual situation — don't just default to managed because it feels safer.

Related

Need help shipping something like this? Get in touch.