DEV Community

Ugur Aslim
Ugur Aslim

Posted on • Originally published at uguraslim.com

Vercel vs. Render vs. Cloudflare Pages for FastAPI + React: Picking the Right Host When Your SaaS Needs PostgreSQL and Real-Time Webhooks

Vercel vs. Render vs. Cloudflare Pages for FastAPI + React: Picking the Right Host When Your SaaS Needs PostgreSQL and Real-Time Webhooks

I've deployed CitizenApp—a production SaaS with 100+ multi-tenant customers, 9 AI features, and Stripe integration—across all three platforms. Each one burned me in a different way. Here's what I learned, and why the "cheapest" option cost me the most.

The Problem Nobody Talks About

When you're building a full-stack SaaS with FastAPI + React, you don't just need a host. You need:

  • Persistent database connections (PostgreSQL)
  • Reliable webhook ingestion (Stripe, webhooks from your own API)
  • Sub-100ms response times for user-facing API calls
  • CI/CD that doesn't require a second job to maintain
  • Predictable billing (no surprise $5,000 overage invoices)

Most hosting comparisons treat these as checkboxes. They're not. They're architectural constraints that force you into a corner.

Vercel: Brilliance and Brokenness

I started with Vercel because it's the obvious choice for Next.js developers. The DX is phenomenal: push to main, and your React site is live in 60 seconds.

But here's what nobody tells you: Vercel's serverless model is fundamentally at odds with persistent database connections.

Why Vercel breaks with FastAPI

FastAPI on Vercel requires Python support. Vercel has it, but with caveats:

# FastAPI on Vercel (works, but...)
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.pool import NullPool

app = FastAPI()

# This is the trap
engine = create_engine(
    "postgresql://...",
    poolclass=NullPool  # No connection pooling—disaster for concurrent requests
)

@app.post("/webhook/stripe")
async def handle_stripe():
    # Each serverless function invocation gets a new connection
    # At scale, PostgreSQL kills you with "too many connections"
    pass
Enter fullscreen mode Exit fullscreen mode

Here's the reality: each Vercel serverless invocation is a fresh process. Connection pooling—essential for PostgreSQL under load—doesn't survive across invocations. I watched CitizenApp's Stripe webhooks fail silently because PostgreSQL hit the connection limit during a traffic spike.

The fix? Add PgBouncer or a connection pool service ($30/month minimum). Now you're running Vercel + a managed pool service + Vercel for the React frontend. The simplicity evaporates.

Cost at scale: Vercel Functions: $0.50/million requests + database tier ($30+) + connection pool ($30+) = $60/month baseline, but overage charges on Functions can hit $200+ unexpectedly.

What Vercel is great for

Vercel dominates for your React frontend. Cloudflare Pages is cheaper, but Vercel's Edge Middleware and ISR (Incremental Static Regeneration) are unmatched. I still use Vercel for CitizenApp's React dashboard.

Render: The Managed Sweet Spot (With Latency Tax)

After Vercel burned me, I moved the FastAPI backend to Render. This is where things got interesting.

Render gives you what Vercel doesn't: persistent services, built-in PostgreSQL, and a sane billing model. Here's CitizenApp's current Render setup:

# render.yaml
services:
  - type: web
    name: citizenapp-api
    env: python
    buildCommand: "pip install -r requirements.txt"
    startCommand: "uvicorn main:app --host 0.0.0.0 --port 8000"
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: citizenapp-db
          property: connectionString
    plan: starter  # $7/month (but goes to standard at scale)

databases:
  - name: citizenapp-db
    plan: starter  # $7/month
    postgresVersion: "15"
Enter fullscreen mode Exit fullscreen mode

Why this works: Render's web services are traditional long-running containers, not serverless. Your FastAPI instance stays alive, connection pooling survives, webhook ingestion is rock-solid.

But—and this is the gotcha—Render's PostgreSQL lives in their infrastructure, not your VPC. This adds latency.

Real numbers from CitizenApp

// React → Render API (East Coast)
// Average latency: 45ms
// P95 latency: 120ms

// Render API → Render PostgreSQL
// Average latency: 8ms (same data center)
// P95 latency: 15ms

// Total user request time: ~60-70ms (acceptable)
Enter fullscreen mode Exit fullscreen mode

Cost: Render starter tier = $7 (API) + $7 (DB) = $14/month for low traffic. Standard tier (needed at ~50 req/sec) = $25 + $15 = $40/month.

The problem emerges at scale. Render's pricing is transparent but steep once you hit standard tier. I've seen bills climb to $200+/month for a mid-sized SaaS.

Cloudflare Pages: The Trap

I spent two weeks exploring Cloudflare Pages because their pricing is seductive: $20/month flat for unlimited everything.

Here's the truth: Cloudflare Pages cannot run FastAPI.

Workers? Yes. Hono? Yes. Full Python FastAPI? No. You'd need to:

  1. Deploy FastAPI elsewhere (Render, Railway, etc.)
  2. Use Cloudflare Pages for the React frontend
  3. Route API calls through Cloudflare Workers (adding complexity and latency)

This defeats the purpose of choosing Cloudflare. You're back to managing two hosts.

Exception: If your SaaS is 90% static content + light API calls, Cloudflare Pages + a lightweight serverless function might work. But for database-heavy multi-tenant apps? Don't bother.

The Architecture I Actually Use Now

After burning money and time, here's what CitizenApp runs on:

┌─────────────────────────────────────────┐
│  Vercel (React frontend)                │
│  - Dashboard, auth flows, client-side   │
│  Cost: ~$20/month (prorated at usage)   │
└──────────────┬──────────────────────────┘
               │ (API calls)
┌──────────────▼──────────────────────────┐
│  Render (FastAPI backend)               │
│  - 2 standard web services ($25 each)   │
│  - 1 PostgreSQL standard ($15)          │
│  - Redis for caching ($7)               │
│  Cost: ~$72/month baseline              │
└─────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

This costs me ~$90/month for production infrastructure. It's not the cheapest, but it's the cheapest that actually works.

Gotcha: What I Missed

I assumed Render's PostgreSQL backup strategy was sufficient. It isn't—not for a SaaS handling payment data. I now run:

# Daily backups to S3 (CitizenApp's actual backup strategy)
import boto3
import subprocess
from datetime import datetime

def backup_postgres():
    timestamp = datetime.utcnow().isoformat()
    backup_file = f"/tmp/citizenapp-{timestamp}.sql"

    subprocess.run([
        "pg_dump",
        os.environ["DATABASE_URL"],
        "-f", backup_file,
        "--verbose"
    ])

    s3 = boto3.client("s3")
    s3.upload_file(
        backup_file,
        "citizenapp-backups",
        f"postgres/{timestamp}.sql"
    )
Enter fullscreen mode Exit fullscreen mode

Render's backups are good for accidental DROP TABLE. They're not good for ransomware or catastrophic infrastructure failure. Add S3 backups ($1/month).

The Verdict

Choose Render if: You need a reliable, scalable backend with PostgreSQL, and you can justify $70–150/month.

Choose Vercel if: You're frontend-only, or you have a serverless-friendly architecture (API Gateway + Lambda + DynamoDB).

Don't choose Cloudflare Pages if: Your backend is anything heavier than a toy project.

For most SaaS founders, Render is the honest choice. It's not the cheapest. But it won't wake you up at 2 AM with a "too many connections" error.

Top comments (0)