Engineering

Why We Chose File-Based Storage Over PostgreSQL (And Saved $50/Month)

A case study on choosing a lightweight JSON file store over a managed PostgreSQL instance for DeveloperLab — cutting cloud overhead while maintaining sub-millisecond read times for a read-heavy API directory workload.

Every infrastructure decision carries an implicit assumption about your workload. When we started building DeveloperLab, the default assumption was "we'll need a database" — and the default database was going to be a managed PostgreSQL instance on a major cloud provider.

We priced it out. Smallest viable Postgres instance with automated backups: $48–56/month. For a project in early development, that felt like betting on an architecture before understanding the actual access patterns.

So we did the boring-but-important thing: we studied our reads and writes first.

The Actual Access Pattern

DeveloperLab's API directory has a fundamentally read-heavy, write-rare workload:

Operation Frequency Source
List all APIs (filtered/sorted) ~5,000 req/day User browsing
Get single API detail ~800 req/day Detail pages
Write API health data 72 writes/day Health checker (every 20 min)
Write API metadata ~0.1 writes/day Manual catalog updates

The ratio of reads to writes is roughly 80:1. This is the worst possible fit for a row-locking, WAL-journaling relational database. Postgres excels when you have complex join queries, transactional integrity requirements, and concurrent write contention. We have none of those.

What We Use Instead: In-Memory JSON with File Persistence

Our data store is a single public_apis.json file (~2 MB) loaded entirely into memory on startup. Every read is a JavaScript array filter — no network round-trip, no query parser, no connection pool.

// Zero-latency read — data is already in process memory
export function loadStoredData(): StoredData {
  return cachedData ?? JSON.parse(readFileSync(DATA_FILE, "utf-8"));
}

// Atomic write — replace the whole file
export function saveStoredData(data: StoredData): void {
  writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), "utf-8");
  cachedData = data;
}

The read latency is essentially zero — we're doing a JavaScript array operation in the same process. A Postgres query over a network connection, even a fast one, adds 1–5 ms of overhead per request. Doesn't sound like much, but across 5,000 requests/day it adds up.

Benchmarks

We ran wrk against both implementations during a local benchmark:

Implementation p50 latency p99 latency Requests/sec
Managed Postgres (same VPC) 4.2 ms 18.7 ms 2,400
In-memory JSON file 0.3 ms 1.1 ms 31,000

The JSON store is 14× faster at p50 for our specific access pattern. This isn't a universal result — if we needed full-text search, complex aggregations, or multi-table joins, Postgres would win decisively. But for "filter an array of 200 objects by category and sort by name," JavaScript is unbeatable.

The Tradeoffs We Accept

We're not pretending this is universally correct. The tradeoffs are real:

We accept:

We reject for our workload:

When to Reach for Postgres Instead

If any of these are true for your project, use Postgres:

DeveloperLab has none of these. Our "database" is 199 JSON objects loaded at startup. The right tool for the right job.


The total infrastructure savings over 12 months: ~$600. More importantly, there's nothing to provision, rotate credentials for, or wake up at 2 AM about.