Skip to main content
ship before you scale

The Graduation Checklist: When Each Component Needs Its Own Server

6 min read Chapter 42 of 42

The Graduation Checklist

Graduation means moving a component from the shared single-server stack to its own dedicated resource. It is not a celebration. It is a cost increase. Each graduation should be delayed as long as possible and executed only when metrics confirm the shared resource is the bottleneck.

The Feature

A checklist for each component that answers three questions:

  1. When: What metric threshold triggers graduation?
  2. How: What are the exact steps?
  3. How much: What does it cost?

The Decision

Graduate one component at a time. Never graduate two components simultaneously. Each graduation changes the system topology, and debugging issues is easier when only one variable changed. The order of graduation, based on which component typically hits its limits first in a growing SaaS, is:

  1. Database (Supabase Free → Pro)
  2. VPS (CX22 → CX32)
  3. Redis (colocated → separate)
  4. Email (Resend Free → Pro)
  5. Observability (Grafana Cloud Free → Pro)

Most SaaS products at the $10K MRR milestone have graduated only items 1 and 2.

The Implementation

Graduation 1: Database (Supabase Free → Pro)

Trigger: Database size exceeds 400 MB, or query performance degrades during peak hours, or connection count exceeds 40.

Steps:

  1. Upgrade the Supabase project to Pro in the dashboard
  2. No code changes required (same connection string)
  3. Verify the application works with the new tier
  4. Enable Point-in-Time Recovery (PITR) for backups

Cost: +$25/month Rollback: Downgrade in Supabase dashboard (data is preserved)

# Verify database connection after upgrade
docker compose exec backend python -c "
import asyncio
import asyncpg

async def check():
    conn = await asyncpg.connect('$DATABASE_URL')
    version = await conn.fetchval('SELECT version()')
    print(f'Connected: {version}')
    await conn.close()

asyncio.run(check())
"

Graduation 2: VPS (CX22 → CX32)

Trigger: CPU above 70% sustained during business hours, or memory above 80% with cache evictions, or disk above 70%.

Steps:

  1. Create a snapshot of the CX22 in Hetzner Cloud console
  2. Create a new CX32 server from the snapshot
  3. Verify all Docker containers start and the application works
  4. Update Cloudflare DNS A record to the new server IP
  5. Wait for DNS propagation (typically 1-5 minutes with Cloudflare proxy)
  6. Delete the old CX22 server

Cost: +€3/month (€4.51 → €7.49) Rollback: Update DNS back to the old server IP (keep the old server running for 24 hours before deleting)

# On the new CX32 server, verify everything is running
docker compose ps
docker compose logs --tail 50 backend
curl -s http://localhost:8000/health | jq

Graduation 3: Redis (Colocated → Separate)

Trigger: Redis memory at 90% of allocation, or cache eviction rate is high (visible in INFO stats as evicted_keys), or application and Redis compete for memory causing swap usage.

Steps:

  1. Create a Hetzner CX11 server (€3.79/month)
  2. Install Docker, run Redis with 1 GB max memory
  3. Set up Hetzner private network between the CX32 and CX11
  4. Update REDIS_URL in the application’s environment variables to point to the new Redis server’s private IP
  5. Restart the application
  6. Verify cache is working (check cache hit rate in application logs)

Cost: +€3.79/month Rollback: Revert REDIS_URL to redis://localhost:6379 and restart

# docker-compose for Redis on the dedicated server
services:
  redis:
    image: redis:7-alpine
    command: >
      redis-server
      --maxmemory 1gb
      --maxmemory-policy allkeys-lru
      --bind 0.0.0.0
      --requirepass ${REDIS_PASSWORD}
      --save ""
      --appendonly no
    ports:
      - "10.0.0.2:6379:6379" # Private network only
    restart: unless-stopped

Graduation 4: Email (Resend Free → Pro)

Trigger: Approaching 100 emails per day consistently.

Steps:

  1. Upgrade in Resend dashboard
  2. No code changes required

Cost: +$20/month Rollback: Downgrade in Resend dashboard (emails already sent are not affected)

Graduation 5: Observability (Grafana Cloud Free → Pro)

Trigger: Active series count approaching 10,000, or 14-day retention is insufficient for debugging.

Steps:

  1. Upgrade in Grafana Cloud dashboard
  2. No code changes required

Cost: +$29/month (Grafana Cloud Pro) Rollback: Downgrade in dashboard

The Full Graduation Timeline

Month 1-3:   Everything on free tiers and CX22
             Infrastructure: ~$10/month
             Revenue: $0-$290/month

Month 4-6:   Database graduated to Supabase Pro
             Infrastructure: ~$35/month
             Revenue: $290-$870/month

Month 7-12:  VPS graduated to CX32
             Infrastructure: ~$38/month
             Revenue: $870-$2,900/month

Month 12-18: Redis separated to its own VPS
             Infrastructure: ~$42/month
             Revenue: $2,900-$5,800/month

Month 18+:   Consider: Resend Pro, Grafana Pro
             Infrastructure: ~$90/month
             Revenue: $5,800+/month

This timeline is approximate. A product that grows faster will graduate components sooner. A product that grows slower may never need to graduate Redis to its own server. The point is that each graduation is a response to measured demand, not a proactive architectural decision.

The End of the Cheap Stack

The cheap stack ends when the product requires:

  • Multiple application servers (load balancer, session management)
  • Read replicas (for separating read and write database traffic)
  • Dedicated background workers (for processing queues)
  • Multi-region deployment (for global latency requirements)

Each of these is a significant architectural change. They are not covered in this book because reaching the point where they are necessary means the product has succeeded. The revenue at that scale (hundreds of paying customers) justifies hiring engineers who specialize in infrastructure.

The cheap stack’s purpose is to get from zero to validation. From idea to paying customers. From “I think this could work” to “I know this works, and here are the metrics to prove it.” Everything after that is a scaling problem, and scaling problems are good problems to have.

The Trap

# TRAP: Graduating all components at once because "we're growing"
# Month 6: Revenue is $500/month
# Developer upgrades: Supabase Pro ($25), CX32 (+$3), separate Redis (+$3.79),
#   Resend Pro ($20), Grafana Pro ($29)
# Infrastructure: $86/month
# Infrastructure is now 17% of revenue
# None of these upgrades were triggered by actual metrics

# SAFE: Graduate one at a time, only when metrics require it
# Month 6: Revenue is $500/month
# Database is at 350 MB (70% of 500 MB limit) - YELLOW, investigate but no action
# VPS CPU at 30%, memory at 45% - GREEN
# Redis memory at 60% - GREEN
# Infrastructure: $10/month (2% of revenue)
# No graduation needed

The Cost

This section costs nothing. It is a decision framework. The actual cost comes only when a graduation is triggered, and the previous sections document those costs precisely.

The total maximum infrastructure cost after all graduations, at approximately 500 paying customers and $14,500/month in revenue, is $112/month. That is a 99.2% margin on infrastructure. The cheap stack is not just a bootstrapping strategy. It is a high-margin business strategy.