DEV Community

Niuniu Ox
Niuniu Ox

Posted on

I Moved My Entire Stack from a $20/Month VPS to Cloudflare Free Tier — 30-Day Bill: $0

I Moved My Entire Stack from a $20/Month VPS to Cloudflare Free Tier — 30-Day Bill: $0

Last month I was paying $20/month for a VPS that mostly sat idle. This month I'm paying $0. Here's exactly what I moved and how.

The Setup I Replaced

Service Before After Monthly Cost
API backend 2GB VPS (Ubuntu) Cloudflare Workers $20 → $0
Static site Same VPS Cloudflare Pages included → $0
Cron jobs Cron on VPS Cloudflare Cron Triggers included → $0
Database SQLite on VPS Turso (9GB free) included → $0

Total: $240/year → $0/year

What I Actually Moved

1. API Backend → Cloudflare Workers

My old Express.js server handled ~50K requests/month. I rewrote it as a Worker:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname === '/api/health') {
      return new Response(JSON.stringify({ status: 'ok', timestamp: Date.now() }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    if (url.pathname === '/api/users' && request.method === 'GET') {
      const { results } = await env.DB.prepare('SELECT * FROM users LIMIT 100').all();
      return Response.json(results);
    }

    return new Response('Not found', { status: 404 });
  }
};
Enter fullscreen mode Exit fullscreen mode

Cold start: 0ms (vs ~200ms on VPS with PM2 restarts)

Free tier limit: 100,000 requests/day — I use ~1,700/day

2. Cron Jobs → Cloudflare Cron Triggers

I had 3 cron jobs: daily backups, weekly reports, hourly health checks.

// wrangler.toml
[triggers]
crons = ["0 2 * * *", "0 9 * * MON", "0 * * * *"]

// worker.js
export default {
  async scheduled(event, env, ctx) {
    switch(event.cron) {
      case "0 2 * * *":
        await runBackup(env);
        break;
      case "0 9 * * MON":
        await generateWeeklyReport(env);
        break;
      case "0 * * * *":
        await healthCheck(env);
        break;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

VPS cron problems I don't miss:

  • Server timezone drift (my backups ran at 3 AM instead of 2 AM for a week)
  • No retry logic — if the server hiccupped, the job just didn't run
  • Logs scattered across /var/log/syslog

3. Database → Turso (SQLite at the Edge)

My 200MB SQLite database moved to Turso's free tier:

// libsql client
import { createClient } from '@libsql/client';

const db = createClient({
  url: env.TURSO_URL,
  authToken: env.TURSO_AUTH_TOKEN
});

// Same SQL, works everywhere
const users = await db.execute('SELECT * FROM users WHERE active = 1');
Enter fullscreen mode Exit fullscreen mode

Free tier: 9GB storage, 500 databases, 1 billion row reads/month

The Gotchas

Cloudflare Workers limits:

  • 10ms CPU time per request (free tier) — fine for APIs, not for video encoding
  • No WebSocket support on free tier (I don't use them)
  • Environment variables encrypted at rest, but you can't log them

Turso limits:

  • 9GB total across all databases — plenty for my use case
  • No persistent connections (HTTP-based) — adds ~5ms latency vs local SQLite

Performance Comparison

Metric VPS ($20/mo) Cloudflare Free
API response time (p50) 45ms 12ms
API response time (p99) 180ms 45ms
Uptime 99.2% (3 restarts) 100% (30 days)
Deploy time 2 min (git pull + restart) 15 sec (git push)

What I Still Pay For

  • Domain: $12/year (Cloudflare Registrar, at-cost pricing)
  • Email forwarding: $0 (Cloudflare Email Routing, free)

The Real Win Isn't the Money

It's the operational simplicity. No SSH, no apt update, no worrying about disk space, no 3 AM "server is down" alerts. My infrastructure is now 3 files in a Git repo.


What's your experience with serverless free tiers? Have you hit any limits that forced you back to a VPS?

P.S. If you're looking for more free dev tools, I maintain a list at Free Dev Resources — no signup required.

Top comments (0)