Thomas Cannon

The lowest latency combination between Hetzner ARM and Crunchy Bridge

tl;dr: The best combination of Hetzner's ARM servers and Crunchy Bridge's regions are:

  • Hetzner: nbg1-dc3
  • Crunchy Bridge:  AWS eu-central-1
  • Benchmarks at the end of the post

I’ve been working on getting the first Practical Computer app out of beta (a dispatcher app for service companies). It, and the rest of our smaller apps, have the following constraints:

  • Extremely shoestring budget. $30/month operating budget. Not much to work with!
  • Since it’s production data, there’s responsibility for the data to be redundant, safe, and secure.

Given those, I ended up choosing Hetzner’s ARM servers, given the sheer power + cost combination (even with them being in the EU; and this is a US-based project).

I’ve had great experiences with the Crunchy Bridge team, and 18+ years of experience have taught me that I really want someone else to make sure the database is secure for me. So even though it’s 10% of my operating budget here, it’s worth it (because the database is the most crucial part of the app!).

The problem is that the app server & database are on two separate networks; I don’t get the benefit of an internal network, so I had to benchmark what combination of regions would work best.

The previous iteration was completely misconfigured: Hetzner’s Helsinki region for the app, AWS eu-north–1 for Crunchy Bridge. This turned out to be awful for performance. After some benchmarking and experimenting, I found that the following combination gets pretty dang close to good performance given the constraints:

  • Hetzner: nbg1-dc3
  • Crunchy Bridge: AWS eu-central–1

(I’ve got benchmarks at the end of the blog post, don’t worry!)

Lessons learned

I wanted to write this not only to help other folks find the best combination easily; but to talk about the mistakes I made and lessons I learned.

  • Mistake: I should have benchmarked different options & done more research. My first Crunchy Bridge cluster was actually in eu-central–1 , with the Hetzner server in Helsinki; and if I’d benchmarked & explored my options I would have avoided an unnecessary database migration.
  • Win: When spooling up the Hetzner server, I did my best to use Ansible to provision the server, and use Dokku for a PaaS-like experience. This made it so that spooling up a new server in nbg1-dc3 was significantly easier. This is why I advocate for “automate as you go”, especially for infrastructure.
  • Win: choosing the right vendors. Hetzner allows me to quickly & cheaply spool up a small project, and having Crunchy Bridge’s support & their tooling made spooling up the replicas & promoting them dead-simple.

Alternative solves

To be clear, I had a few other options available to me, some of which I could still choose, and have implemented:

  • Application-level caching: This is done, and basically a given for any production app. I ended up using Redis here.
  • Making a read replica on the Hetzner instance; use that for reads (a heroically misguided, but technically feasible idea!)
  • Move from Hetzner to AWS. This would only make sense for an app that’s actually generating the profit to justify the switch. It’s hard to bear Hetzner’s price & power combo here.
  • Move the primary DB back to Hetzner (not ideal. Again, 18+ years of experience have taught me that I don’t want to manage a bunch of databases)

Benchmarks, as promised

# Benchmark script
# x.report("crunchy-eu-central") { ApplicationRecord.connected_to(role: :crunchy-eu-central){ Organization.first.onsites }}
# x.report("crunchy-eu-north-1") { ApplicationRecord.connected_to(role: :crunchy-eu-north-1){ Organization.first.onsites } }

hetzner-helsinki:~$ dokku run dispatcher rails@aaaaaa:/rails$ ruby script/benchmarks/database_performance.rb … ruby 3.3.1 (2024-04-23 revision c56cd86388) +YJIT [aarch64-linux] Warming up ————————————– crunchy-eu-central 1.000 i/100ms crunchy-eu-north-1 1.000 i/100ms Calculating ————————————- crunchy-eu-central 9.788 (±10.2%) i/s - 49.000 in 5.031111s crunchy-eu-north-1 14.124 (± 7.1%) i/s - 71.000 in 5.045164s

Comparison: crunchy-eu-north-1: 14.1 i/s crunchy-eu-central: 9.8 i/s - 1.44x slower

====================================================================================================================

hetzner-nuremberg:~$ dokku run dispatcher rails@5e9d1df43591:/rails$ ruby script/benchmarks/database_performance.rb … ruby 3.3.1 (2024-04-23 revision c56cd86388) +YJIT [aarch64-linux] Warming up ————————————– crunchy-eu-central 1.000 i/100ms crunchy-eu-north-1 7.000 i/100ms Calculating ————————————- crunchy-eu-central 20.452 (± 0.0%) i/s - 103.000 in 5.037650s crunchy-eu-north-1 96.340 (± 2.1%) i/s - 483.000 in 5.016063s

Comparison: crunchy-eu-north-1: 96.3 i/s crunchy-eu-central: 20.5 i/s - 4.71x slower

====================================================================================================================

/// Spot-check of query latency

hetzner-helsinki:~$ dokku run dispatcher rails@aaaaaa:/rails$ bin/rails c Loading production environment (Rails 7.1.3.2) irb(main):001> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-central){ Organization.first.onsites } } } 1637.3784099705517 103.8463581353426 106.55084392055869 105.82964192144573 102.70471591502428 102.40731597878039 107.70620591938496 112.95577604323626 111.48629407398403 105.25572090409696 => 10 irb(main):002> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-north-1){ Organization.first.onsites } } } 1040.9399520140141 72.33657804317772 73.68614105507731 77.82750786282122 75.32874401658773 73.2035799883306 77.43134815245867 72.01353716664016 71.68629695661366 74.11726214922965 => 10

====================================================================================================================

hetzner-nuremberg:~$ dokku run dispatcher rails@aaaaaa:/rails$ bin/rails c Loading production environment (Rails 7.1.3.2) irb(main):001> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-central){ Organization.first.onsites } } } 846.7416610001237 54.84945299758692 56.269697000971064 55.53825500101084 54.511651000211714 54.32877099883626 57.52018099883571 59.374027001467766 63.36915899737505 55.06629299998167 => 10 irb(main):002> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-north-1){ Organization.first.onsites } } } 168.30048900010297 11.614236002060352 12.266477999219205 15.882210002018837 15.326369000831619 11.819797000498511 14.219884997146437 10.650393000105396 11.742956001398852 13.63104199845111 => 10