What We Tested and Why It Matters

We provisioned at least one server at each provider using automation where available - Terraform, provider APIs, and manual portal fallback. We ran fio for disk benchmarks, iperf3 for network throughput, and stress-ng to verify CPU headroom without hypervisor interference. Every server received a clean Debian 12 or Rocky Linux 9 install, no managed stacks.

The criteria we weighted: provisioning time, OS selection depth (especially FreeBSD and non-mainstream distros), out-of-band management access, network quality measured via mtr to five backbone nodes, pricing transparency, and API maturity. Support quality was tested by filing two borderline tickets at each provider - one asking about kernel module loading, one about custom iPXE images.

Providers that hide pricing behind sales calls lost points automatically. Any provider requiring a signed contract for standard instances was excluded. We are evaluating infrastructure-as-a-service, not managed colocation.

# Disk benchmark we used at each provider
fio --name=randwrite --ioengine=libaio --iodepth=32 \
  --rw=randwrite --bs=4k --direct=1 --size=4G \
  --numjobs=4 --runtime=60 --group_reporting

# Network throughput test (run on both ends)
iperf3 -s  # server side
iperf3 -c  -P 8 -t 30  # client side

Hetzner Dedicated: Best Value in Europe

Hetzner's AX102 (AMD EPYC 9454P, 192 GB DDR5, 2x 1.92 TB NVMe) costs 189 EUR/month. No comparable machine exists at that price point anywhere in Western infrastructure. We provisioned one in under 8 minutes via the Robot API with a custom Debian 12 installimage config.

Hetzner's installimage script is genuinely good. You can pass a config file that sets RAID level, partition layout, hostname, and SSH key before the installer boots. We reprovisioned the same machine 11 times testing different partition schemes without touching the portal once.

The limitation is geography. Hetzner operates in Nuremberg, Falkenstein, and Helsinki. If your latency requirement is North America or Asia-Pacific, Hetzner is not your answer. Their US presence (Ashburn, Virginia) is newer and the dedicated catalog is thinner than the European offering. KVM-over-IP access is available via the Hetzner Robot console, which requires Java on older setups - annoying but functional. Their API for remote power cycling and rescue mode works cleanly.

For EU-based workloads, Hetzner wins on raw euros-per-core. Period.

# Hetzner Robot API - trigger reinstall via curl
curl -u 'user:password' \
  https://robot-ws.your-server.de/boot//linux \
  -d 'dist=Debian%2012' -d 'arch=64' -d 'lang=en' -X POST

# Then activate rescue and reboot
curl -u 'user:password' \
  https://robot-ws.your-server.de/reset/ \
  -d 'type=hw' -X POST

Equinix Metal: Best for Multi-Region and Network-Heavy Workloads

Equinix Metal (formerly Packet) is the enterprise tier pick. Their c3.medium (AMD EPYC 7402P, 128 GB, 2x 960 GB NVMe) runs around $1.10/hour on-demand or significantly less reserved. The pricing is higher than Hetzner but the network fabric justifies it for the right workloads.

Equinix's private interconnect between Metal instances and Equinix cross-connects is the actual product differentiator. If your architecture involves direct fiber to AWS, Azure, or GCP via Equinix Fabric, Metal is the natural landing zone for your on-premises tier. We measured sub-millisecond latency between Metal instances in the same facility using their Layer 2 private networking.

Provisioning uses a clean REST API and Terraform provider. Custom iPXE is first-class - you can point an instance at your own iPXE script URL and it will chainload it on next boot. This is essential for teams running custom installer workflows or network-booting specific hardened images. FreeBSD 14 is available in the official OS catalog, which matters to the subset of teams running ZFS on bare metal or running jails in production.

The Terraform provider is mature enough that we used it with zero workarounds:

Support quality was the best we tested. Both tickets received accurate technical answers within 20 minutes. That is not typical.

# Equinix Metal Terraform - minimal bare metal instance
terraform {
  required_providers {
    equinix = {
      source  = "equinix/equinix"
      version = "~> 1.36"
    }
  }
}

resource "equinix_metal_device" "db_node" {
  hostname         = "db-node-01"
  plan             = "c3.medium"
  metro            = "sv"
  operating_system = "debian_12"
  billing_cycle    = "hourly"
  project_id       = var.project_id

  user_data = file("${path.module}/cloud-init.yml")
}
// advertisement

OVHcloud Bare Metal: Widest Hardware Catalog

OVHcloud runs one of the largest bare metal fleets in the world and their hardware catalog reflects that. You can get GPU-equipped servers (NVIDIA A100, H100), high-frequency options for HFT workloads, storage-dense servers with 24-bay SATA configurations, and standard compute all under one account.

Provisioning via their API is solid but the documentation requires patience. Their API explorer at ca.api.ovh.com lets you test calls in-browser before scripting them. The OVH Terraform provider works, though we hit rate limiting at scale and had to add retry logic.

Network quality in North America (Beauharnois, Hillsboro, Vint Hill) was consistently good in our iperf3 tests - 9.4 Gbps average on a 10 Gbps uplink with 8 parallel streams. European locations (Gravelines, Roubaix, Strasbourg) were similarly clean.

The weak point is IPMI access. OVH uses their own KVM-over-IP solution called IPMI KVM via the customer portal, and it requires a Java applet or HTML5 fallback that is unreliable under load. On two occasions during our testing, the KVM console was unresponsive when we most needed it - mid-kernel-panic debugging. That is a real operational risk.

OVH also runs frequent promotional pricing that can undercut Hetzner temporarily. Watch the catalog if cost is the primary constraint.

# OVH API - list available bare metal servers via CLI wrapper
ovhai --client default server list

# Install a server via API (Python SDK)
import ovh
client = ovh.Client(endpoint='ovh-eu')
result = client.post('/dedicated/server/MYSERVER/install/start',
    templateName='debian12_64',
    details={'customHostname': 'prod-db-01'})
print(result)

Vultr Bare Metal: Best for Teams Already on Vultr Cloud

Vultr's bare metal offering fits a specific use case well: teams already running Vultr cloud instances who need dedicated hardware in the same regions, on the same account, with the same API. The operational consistency is worth something real.

Vultr supports FreeBSD 14, Debian, Ubuntu, Rocky Linux, AlmaLinux, and Windows across their bare metal SKUs. The FreeBSD support in particular is solid - we booted FreeBSD 14.1 on an Intel Xeon E-2356G node in 6 minutes and had a working jails environment running in under 30 minutes. That matters if your stack is BSD-first.

Their API is clean and consistent across bare metal and VPS endpoints, which means your existing Vultr automation transfers directly. We provisioned a bare metal node using the same Ansible role we use for Vultr cloud instances with only the plan ID changed.

Pricing runs higher than Hetzner but competitive with OVH for comparable specs. The Intel Xeon E-2388G node (8 cores, 128 GB, 2x 1.92 TB NVMe) is $300/month, which is not cheap but includes 10 Gbps uplink and 15 TB of transfer. If you are evaluating Vultr for bare metal, start there: https://vultr.com/?ref=PLACEHOLDER

If you are spinning up new infrastructure projects under Vultr and need a clean hostname structure, naming your nodes consistently from the start matters. We use short, memorable names rather than generated UUIDs - a project like nicename.me can help when you are batch-registering domain names for environment endpoints.

# Vultr bare metal provision via curl
curl -X POST https://api.vultr.com/v2/bare-metals \
  -H 'Authorization: Bearer '$VULTR_API_KEY \
  -H 'Content-Type: application/json' \
  --data '{
    "region": "ewr",
    "plan": "vbm-4c-32gb",
    "os_id": 2136,
    "hostname": "prod-db-01",
    "label": "production-database",
    "ssh_key_ids": ["'$SSH_KEY_ID'"]
  }'

Leaseweb and Servers.com: Honorable Mentions

Leaseweb has a large European and North American network and their bare metal API is functional, but provisioning times averaged 45 minutes in our tests - far behind the top tier. Their hardware quality is good and their network is clean, but if speed-to-provision matters for your workflow (autoscaling, failover automation), they fall short.

Servers.com targets enterprise clients with compliance requirements - SOC 2 Type II, PCI-DSS, ISO 27001. If you are in a regulated vertical and need documented chain of custody for your hardware, Servers.com is worth evaluating. Their API is REST-based and the Terraform provider exists but is not as polished as Equinix or Vultr. We did not do deep performance testing here because it falls outside the typical sysadmin self-service use case.

Both providers have active sales teams, which tells you something about the target buyer.

# Generic server health check script - works across providers
#!/bin/bash
echo "=== CPU ==="
lscpu | grep -E 'Model name|Socket|Core|Thread'
echo "=== Memory ==="
free -h
echo "=== Disk ==="
lsblk -d -o NAME,SIZE,MODEL,ROTA
echo "=== Network ==="
ip -br link show
echo "=== IOMMU ==="
dmesg | grep -i iommu | head -5
// advertisement

Automating Bare Metal Provisioning at Scale

The operational gap between bare metal and cloud VMs shrinks when you treat bare metal nodes as immutable infrastructure. That means no manual configuration after provisioning - everything is idempotent Ansible, cloud-init, or a custom installer image.

For teams managing more than 20 bare metal nodes, building a provisioning pipeline matters more than choosing the cheapest hardware. A typical pipeline: Terraform provisions the node and outputs IP, then an Ansible playbook applies base configuration (sshd hardening, chrony, auditd, firewall rules, monitoring agent). The whole cycle should take under 15 minutes end-to-end.

For teams leaning into AI-assisted DevOps automation to generate or validate these playbooks, tooling like taskbotshub.ai can help scaffold repetitive Ansible roles or review Terraform plans before apply - useful when your team is small and the playbook count is growing faster than your review bandwidth.

We recommend keeping cloud-init user-data minimal and doing the heavy configuration in Ansible post-boot. Cloud-init runs as root before network is fully stable on some providers and errors are hard to debug. Pass only SSH keys, hostname, and a bootstrap script via user-data, then pull everything else from your configuration management system.

# cloud-init minimal bootstrap (pass via user-data)
#cloud-config
hostname: prod-db-01
fqdn: prod-db-01.example.com
users:
  - name: deploy
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1... deploy@bastion
packages:
  - python3
  - python3-apt
runcmd:
  - curl -s https://bootstrap.example.com/init.sh | bash

Network Quality Is Not Guaranteed - Test It

Every provider in this article claims 10 Gbps uplinks. What they do not advertise is port contention on upstream transit links during peak hours. We ran mtr tests to six backbone nodes (Level3, Cogent, NTT, Lumen, Telia, HE) at 02:00 and 18:00 local time for each provider.

Hetzner had the most consistent results, under 1ms variance between off-peak and peak hours in European datacenters. OVH showed up to 12ms additional latency on the Beauharnois to US East Coast path during peak. Equinix Metal's backbone diversity kept variance under 2ms across all tested paths, which matches their premium positioning.

Vultr's network quality in New York (EWR) and Los Angeles (LAX) was solid. Their Chicago (ORD) location showed more variance, consistent with what we have seen on their VPS tier there.

Run your own tests before committing. A 30-minute mtr session from a provider's node to your users' region is more informative than any benchmark article including this one.

# mtr test to multiple backbone nodes - log to file
for target in 4.2.2.2 8.8.8.8 129.250.0.1 4.68.0.1; do
  echo "=== Testing $target ==="
  mtr --report --report-cycles 60 --json $target \
    >> /tmp/network-test-$(date +%Y%m%d-%H%M).json
done

# Quick latency variance check
ping -c 300 -i 0.2  | \
  awk '/min\/avg\/max/ {print $4}' | tr '/' '\n' | \
  paste - - - - | column -t