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-dc3was 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