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:
- Data loss on a hard server crash (health data is re-probed within 20 min)
- No concurrent write safety (we have exactly one writer: the health checker)
- Manual backups via git commit
We reject for our workload:
- $50/month infrastructure overhead
- Connection pool management
- Schema migration tooling
- Operational complexity for a read-heavy cache
When to Reach for Postgres Instead
If any of these are true for your project, use Postgres:
- Multiple concurrent writers (forms, user-generated content)
- Data you cannot afford to lose (financial records, user accounts)
- Complex relational queries (joins, aggregations over millions of rows)
- Audit trails or event sourcing
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.