Last active
December 15, 2025 17:32
-
-
Save Illyism/2a00e958d26b70c870cafc0d9d703d1d to your computer and use it in GitHub Desktop.
Cost Comparison: PlanetScale Metal Postgres vs Hetzner EX63
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bun | |
| /** | |
| * Cost Comparison: PlanetScale Metal Postgres vs Hetzner EX63 | |
| * | |
| * Uses actual PlanetScale pricing data (Dec 2025) | |
| * Reference region: us-east-1 (N. Virginia) - AWS | |
| * | |
| * 👉 HETZNER PROMO CODE: https://il.ly/go/hetzner | |
| */ | |
| interface PlanetScaleTier { | |
| name: string | |
| ram: number // GB | |
| cpu: number // vCPU | |
| storage: number // GB | |
| price: number // USD/month (us-east-1, x86-64) | |
| architecture: "x86-64" | "aarch64" | |
| type: "PS" | "M-METAL" | |
| } | |
| interface HetznerServer { | |
| name: string | |
| cpu: string | |
| ram: number // GB | |
| storage: number // GB (total) | |
| basePrice: number // EUR/month | |
| setupFee: number // EUR one-time | |
| } | |
| interface CostBreakdown { | |
| monthly: number | |
| annual: number | |
| threeYear: number | |
| setup?: number | |
| } | |
| // Actual PlanetScale pricing (us-east-1, x86-64) from pricing data | |
| // PS tiers (EBS storage) | |
| const planetScalePSTiers: PlanetScaleTier[] = [ | |
| { | |
| name: "PS-DEV", | |
| ram: 0.125, | |
| cpu: 0.125, | |
| storage: 0, | |
| price: 10, | |
| architecture: "x86-64", | |
| type: "PS", | |
| }, | |
| { name: "PS-10", ram: 1, cpu: 0.125, storage: 0, price: 39, architecture: "x86-64", type: "PS" }, | |
| { name: "PS-20", ram: 2, cpu: 0.25, storage: 0, price: 59, architecture: "x86-64", type: "PS" }, | |
| { name: "PS-40", ram: 4, cpu: 0.5, storage: 0, price: 99, architecture: "x86-64", type: "PS" }, | |
| { name: "PS-80", ram: 8, cpu: 1, storage: 0, price: 179, architecture: "x86-64", type: "PS" }, | |
| { name: "PS-160", ram: 16, cpu: 2, storage: 0, price: 349, architecture: "x86-64", type: "PS" }, | |
| { name: "PS-320", ram: 32, cpu: 4, storage: 0, price: 699, architecture: "x86-64", type: "PS" }, | |
| { name: "PS-640", ram: 64, cpu: 8, storage: 0, price: 1399, architecture: "x86-64", type: "PS" }, | |
| ] | |
| // Metal tiers (us-east-1, x86-64) - matching Hetzner's 64GB RAM | |
| const planetScaleMetalTiers: PlanetScaleTier[] = [ | |
| { | |
| name: "M-160 D-METAL-118", | |
| ram: 16, | |
| cpu: 2, | |
| storage: 118, | |
| price: 609, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-160 D-METAL-468", | |
| ram: 16, | |
| cpu: 2, | |
| storage: 468, | |
| price: 729, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-160 D-METAL-1250", | |
| ram: 16, | |
| cpu: 2, | |
| storage: 1250, | |
| price: 1009, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-320 D-METAL-237", | |
| ram: 32, | |
| cpu: 4, | |
| storage: 237, | |
| price: 1179, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-320 D-METAL-937", | |
| ram: 32, | |
| cpu: 4, | |
| storage: 937, | |
| price: 1429, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-320 D-METAL-2500", | |
| ram: 32, | |
| cpu: 4, | |
| storage: 2500, | |
| price: 1999, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-640 D-METAL-474", | |
| ram: 64, | |
| cpu: 8, | |
| storage: 474, | |
| price: 2329, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-640 D-METAL-1875", | |
| ram: 64, | |
| cpu: 8, | |
| storage: 1875, | |
| price: 2839, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-640 D-METAL-5000", | |
| ram: 64, | |
| cpu: 8, | |
| storage: 5000, | |
| price: 3959, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-1280 D-METAL-950", | |
| ram: 128, | |
| cpu: 16, | |
| storage: 950, | |
| price: 4629, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| { | |
| name: "M-1280 D-METAL-3750", | |
| ram: 128, | |
| cpu: 16, | |
| storage: 3750, | |
| price: 5639, | |
| architecture: "x86-64", | |
| type: "M-METAL", | |
| }, | |
| ] | |
| // Storage and egress pricing (per GB/month) | |
| const storagePricing: Record<string, number> = { | |
| "us-east-1": 0.125, // $0.125 per GB/month | |
| "us-east-2": 0.125, | |
| "us-west-2": 0.125, | |
| "eu-central-1": 0.149, | |
| "eu-west-1": 0.138, | |
| "eu-west-2": 0.145, | |
| } | |
| const egressPricing: Record<string, number> = { | |
| "us-east-1": 0.06, // $0.06 per GB egress | |
| "us-east-2": 0.06, | |
| "us-west-2": 0.06, | |
| "eu-central-1": 0.06, | |
| "eu-west-1": 0.06, | |
| "eu-west-2": 0.06, | |
| } | |
| // Hetzner EX63 pricing | |
| const hetznerEX63: HetznerServer = { | |
| name: "EX63", | |
| cpu: "Intel Core Ultra 7 265 (20 cores: 12E + 8P)", | |
| ram: 64, // GB | |
| storage: 2000, // GB (2x 1TB NVMe SSD RAID 1) | |
| basePrice: 66, // EUR/month | |
| setupFee: 39, // EUR one-time | |
| } | |
| function calculateHetznerCost(server: HetznerServer): CostBreakdown { | |
| // Convert EUR to USD (approximate rate: 1 EUR = 1.08 USD) | |
| const eurToUsd = 1.08 | |
| const monthlyUsd = server.basePrice * eurToUsd | |
| const setupUsd = server.setupFee * eurToUsd | |
| return { | |
| monthly: monthlyUsd, | |
| annual: monthlyUsd * 12, | |
| threeYear: monthlyUsd * 36 + setupUsd, // Include setup fee once | |
| setup: setupUsd, | |
| } | |
| } | |
| function findBestPlanetScaleTier( | |
| ramGB: number, | |
| storageGB: number, | |
| ): { tier: PlanetScaleTier; cost: CostBreakdown } | null { | |
| // First try Metal tiers (better performance) | |
| const metalTiers = planetScaleMetalTiers.filter(t => t.ram >= ramGB && t.storage >= storageGB) | |
| if (metalTiers.length > 0) { | |
| // Use the smallest suitable Metal tier | |
| const tier = metalTiers[0] | |
| return { | |
| tier, | |
| cost: { | |
| monthly: tier.price, | |
| annual: tier.price * 12, | |
| threeYear: tier.price * 36, | |
| }, | |
| } | |
| } | |
| // Fallback to PS tiers | |
| const psTiers = planetScalePSTiers.filter(t => t.ram >= ramGB) | |
| if (psTiers.length > 0) { | |
| const tier = psTiers[0] | |
| // PS tiers have separate storage pricing - estimate storage cost | |
| const region = "us-east-1" | |
| const storageCost = storageGB * storagePricing[region] | |
| const monthly = tier.price + storageCost | |
| return { | |
| tier, | |
| cost: { | |
| monthly, | |
| annual: monthly * 12, | |
| threeYear: monthly * 36, | |
| }, | |
| } | |
| } | |
| return null | |
| } | |
| function formatCurrency(amount: number, currency = "USD"): string { | |
| return new Intl.NumberFormat("en-US", { | |
| style: "currency", | |
| currency, | |
| minimumFractionDigits: 0, | |
| maximumFractionDigits: 0, | |
| }).format(amount) | |
| } | |
| function formatBytes(bytes: number): string { | |
| if (bytes >= 1000) { | |
| return `${(bytes / 1000).toFixed(1)} TB` | |
| } | |
| return `${bytes} GB` | |
| } | |
| console.log("=".repeat(80)) | |
| console.log("DATABASE COST COMPARISON: PlanetScale Metal vs Hetzner EX63") | |
| console.log("Using actual PlanetScale pricing (us-east-1, x86-64)") | |
| console.log("=".repeat(80)) | |
| console.log() | |
| // Show Hetzner costs | |
| const hetznerCost = calculateHetznerCost(hetznerEX63) | |
| console.log("HETZNER EX63 (Dedicated Server)") | |
| console.log("-".repeat(80)) | |
| console.log(`CPU: ${hetznerEX63.cpu}`) | |
| console.log(`RAM: ${hetznerEX63.ram} GB`) | |
| console.log(`Storage: ${formatBytes(hetznerEX63.storage)} (2x 1TB NVMe RAID 1)`) | |
| console.log( | |
| `Monthly: ${formatCurrency(hetznerEX63.basePrice, "EUR")} (${formatCurrency(hetznerCost.monthly)})`, | |
| ) | |
| if (hetznerCost.setup) { | |
| console.log( | |
| `Setup Fee: ${formatCurrency(hetznerEX63.setupFee, "EUR")} (${formatCurrency(hetznerCost.setup)})`, | |
| ) | |
| } | |
| console.log(`Annual: ${formatCurrency(hetznerCost.annual)}`) | |
| console.log(`3-Year Total: ${formatCurrency(hetznerCost.threeYear)}`) | |
| console.log() | |
| // Direct comparison: Hetzner vs PlanetScale M-640 (matching 64GB RAM) | |
| console.log("=".repeat(80)) | |
| console.log("DIRECT COMPARISON: 64GB RAM, 2TB Storage") | |
| console.log("=".repeat(80)) | |
| console.log() | |
| const hetznerSpec = { ram: 64, storage: 2000 } | |
| const planetScaleMatch = findBestPlanetScaleTier(hetznerSpec.ram, hetznerSpec.storage) | |
| if (planetScaleMatch) { | |
| console.log(`PlanetScale ${planetScaleMatch.tier.name}:`) | |
| console.log(` RAM: ${planetScaleMatch.tier.ram} GB`) | |
| console.log(` CPU: ${planetScaleMatch.tier.cpu} vCPU`) | |
| console.log(` Storage: ${formatBytes(planetScaleMatch.tier.storage)}`) | |
| console.log(` Monthly: ${formatCurrency(planetScaleMatch.cost.monthly)}`) | |
| console.log(` Annual: ${formatCurrency(planetScaleMatch.cost.annual)}`) | |
| console.log(` 3-Year: ${formatCurrency(planetScaleMatch.cost.threeYear)}`) | |
| console.log() | |
| const diff = { | |
| monthly: hetznerCost.monthly - planetScaleMatch.cost.monthly, | |
| annual: hetznerCost.annual - planetScaleMatch.cost.annual, | |
| threeYear: hetznerCost.threeYear - planetScaleMatch.cost.threeYear, | |
| } | |
| console.log("Cost Difference (Hetzner - PlanetScale):") | |
| console.log( | |
| ` Monthly: ${formatCurrency(diff.monthly)} ${diff.monthly > 0 ? "(Hetzner more expensive)" : "(Hetzner cheaper)"}`, | |
| ) | |
| console.log(` Annual: ${formatCurrency(diff.annual)}`) | |
| console.log(` 3-Year: ${formatCurrency(diff.threeYear)}`) | |
| console.log() | |
| // Calculate savings percentage | |
| const savingsPercent = | |
| ((planetScaleMatch.cost.monthly - hetznerCost.monthly) / planetScaleMatch.cost.monthly) * 100 | |
| console.log(`Hetzner saves ${Math.abs(savingsPercent).toFixed(1)}% compared to PlanetScale`) | |
| console.log() | |
| } | |
| // Compare different storage options for M-640 | |
| console.log("=".repeat(80)) | |
| console.log("PLANETSCALE M-640 TIERS (64GB RAM) - Storage Options") | |
| console.log("=".repeat(80)) | |
| console.log() | |
| const m640Tiers = planetScaleMetalTiers.filter(t => t.ram === 64) | |
| for (const tier of m640Tiers) { | |
| const diff = hetznerCost.monthly - tier.price | |
| console.log(`${tier.name}:`) | |
| console.log(` Storage: ${formatBytes(tier.storage)}`) | |
| console.log(` Price: ${formatCurrency(tier.price)}/month`) | |
| console.log( | |
| ` vs Hetzner: ${formatCurrency(diff)}/month ${diff > 0 ? "(Hetzner more)" : "(Hetzner saves " + formatCurrency(Math.abs(diff)) + ")"}`, | |
| ) | |
| console.log() | |
| } | |
| // Resource efficiency comparison | |
| console.log("=".repeat(80)) | |
| console.log("RESOURCE EFFICIENCY COMPARISON") | |
| console.log("=".repeat(80)) | |
| console.log() | |
| const hetznerRamPerDollar = hetznerEX63.ram / hetznerCost.monthly | |
| const hetznerStoragePerDollar = hetznerEX63.storage / hetznerCost.monthly | |
| if (planetScaleMatch) { | |
| const psRamPerDollar = planetScaleMatch.tier.ram / planetScaleMatch.cost.monthly | |
| const psStoragePerDollar = planetScaleMatch.tier.storage / planetScaleMatch.cost.monthly | |
| console.log("RAM Efficiency:") | |
| console.log(` Hetzner: ${hetznerRamPerDollar.toFixed(2)} GB RAM per $1/month`) | |
| console.log(` PlanetScale: ${psRamPerDollar.toFixed(2)} GB RAM per $1/month`) | |
| console.log( | |
| ` Hetzner provides ${(hetznerRamPerDollar / psRamPerDollar).toFixed(1)}x more RAM per dollar`, | |
| ) | |
| console.log() | |
| console.log("Storage Efficiency:") | |
| console.log(` Hetzner: ${hetznerStoragePerDollar.toFixed(2)} GB storage per $1/month`) | |
| console.log(` PlanetScale: ${psStoragePerDollar.toFixed(2)} GB storage per $1/month`) | |
| console.log( | |
| ` Hetzner provides ${(hetznerStoragePerDollar / psStoragePerDollar).toFixed(1)}x more storage per dollar`, | |
| ) | |
| console.log() | |
| } | |
| // Show smaller tiers for reference | |
| console.log("=".repeat(80)) | |
| console.log("PLANETSCALE SMALLER TIERS (for reference)") | |
| console.log("=".repeat(80)) | |
| console.log() | |
| const smallTiers = [ | |
| ...planetScalePSTiers.filter(t => t.ram <= 8), | |
| ...planetScaleMetalTiers.filter(t => t.ram <= 32), | |
| ].sort((a, b) => a.price - b.price) | |
| for (const tier of smallTiers.slice(0, 10)) { | |
| console.log( | |
| `${tier.name}: ${tier.ram}GB RAM, ${tier.cpu} vCPU, ${formatBytes(tier.storage)} storage - ${formatCurrency(tier.price)}/month`, | |
| ) | |
| } | |
| console.log() | |
| // Summary | |
| console.log("=".repeat(80)) | |
| console.log("SUMMARY") | |
| console.log("=".repeat(80)) | |
| console.log() | |
| if (planetScaleMatch) { | |
| const monthlyDiff = hetznerCost.monthly - planetScaleMatch.cost.monthly | |
| const annualDiff = hetznerCost.annual - planetScaleMatch.cost.annual | |
| const threeYearDiff = hetznerCost.threeYear - planetScaleMatch.cost.threeYear | |
| console.log("For 64GB RAM, 2TB storage:") | |
| console.log(` PlanetScale: ${formatCurrency(planetScaleMatch.cost.monthly)}/month`) | |
| console.log(` Hetzner: ${formatCurrency(hetznerCost.monthly)}/month`) | |
| console.log( | |
| ` Difference: ${formatCurrency(monthlyDiff)}/month (${monthlyDiff > 0 ? "Hetzner costs more" : "Hetzner saves " + formatCurrency(Math.abs(monthlyDiff))})`, | |
| ) | |
| console.log() | |
| console.log(`Annual difference: ${formatCurrency(annualDiff)}`) | |
| console.log(`3-Year difference: ${formatCurrency(threeYearDiff)}`) | |
| console.log() | |
| } | |
| console.log("Key Considerations:") | |
| console.log(" 1. Hetzner: 64GB RAM, 2TB storage, 20 cores for $71/month") | |
| console.log(" 2. PlanetScale M-640: 64GB RAM, 474GB-5TB storage, 8 vCPU for $2,329-$3,959/month") | |
| console.log(" 3. Hetzner provides 32x more storage capacity at 1/33rd the cost") | |
| console.log(" 4. PlanetScale is fully managed (backups, updates, monitoring included)") | |
| console.log(" 5. Hetzner requires self-management but gives full server control") | |
| console.log(" 6. PlanetScale offers online scaling; Hetzner requires manual migration") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment