A complete architectural breakdown of Neon's serverless Postgres and Cloudflare's edge data platform — blobs, functions, D1, KV, R2, and when each changes everything.
██████╗ █████╗ ████████╗ █████╗ ██████╗ █████╗ ███████╗███████╗ ██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔════╝ ██║ ██║███████║ ██║ ███████║██████╔╝███████║███████╗█████╗ ██║ ██║██╔══██║ ██║ ██╔══██║██╔══██╗██╔══██║╚════██║██╔══╝ ██████╔╝██║ ██║ ██║ ██║ ██║██████╔╝██║ ██║███████║███████╗ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝
Neon asks: "What if Postgres was serverless-native?" Cloudflare asks: "What if your database lived at the edge, next to your users?" Both solve the "I want managed data without servers" problem — but from radically different starting points. The architectural choices that follow from those two questions diverge completely.
Neon (still available at neon.tech; announced acquisition by Databricks in 2025 while continuing as a developer Postgres product) is not just "Postgres on AWS." It's a ground-up reimagining of how a relational database should work when you don't want to think about instances, storage provisioning, or connection pooling.
┌──────────────────────────────────────────────────────────┐
│ CLIENT APPLICATION │
│ (any Postgres-compatible driver) │
└─────────────────────────┬────────────────────────────────┘
│ standard pg wire protocol
▼
┌──────────────────────────────────────────────────────────┐
│ NEON PROXY LAYER │
│ Connection pooling · Auth · Routing │
│ HTTP-over-SQL for edge environments │
└──────────┬─────────────────────────────────┬─────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────────┐
│ COMPUTE NODE │ │ COMPUTE NODE │
│ (Postgres 16) │ │ (Postgres 16) │
│ Active/Running │ │ SUSPENDED (cold) │
│ ← your primary │ │ ← wakes in ~500ms │
└──────────┬───────────┘ └────────────┬────────────┘
│ WAL (Write-Ahead Log) │
└────────────┬────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ SAFEKEEPER LAYER │
│ Distributed WAL storage (3-node quorum) │
│ Durability guarantee before ack to compute │
└─────────────────────────┬────────────────────────────────┘
│ async page stream
▼
┌──────────────────────────────────────────────────────────┐
│ PAGESERVER │
│ Object storage (S3) + in-memory page cache │
│ Stores ALL historical versions of every page │
│ Enables: branching · PITR · instant forks │
└──────────────────────────────────────────────────────────┘
The most radical thing Neon does. Traditional Postgres fuses compute and storage together — your data lives on the same machine running queries. Neon separates them entirely. Compute nodes (plain Postgres processes) talk to a remote Pageserver over a custom protocol. This means compute can scale to zero, wake up in a few hundred milliseconds (often ~200–500ms depending on region and plan), and multiple computes can read the same storage simultaneously.
The Pageserver stores every page version ever written, backed by S3. This enables Neon's three killer features:
(1) Point-in-time recovery to any second in your retention window,
(2) Database branching that's instant and copy-on-write (no data duplication until you write), and
(3) Time-travel queries with AS OF syntax. Every branch is just a pointer to a historical page version.
Before any write is acknowledged, it's committed to a 3-node Safekeeper quorum — think of it as a distributed WAL buffer. This decouples durability from compute availability. Your compute can crash; the WAL is safe. When a new compute wakes up, it replays from the Safekeeper to get current.
For environments that can't maintain persistent TCP connections (serverless functions, edge runtimes), Neon exposes an HTTP endpoint where you send SQL in JSON. No persistent connection, no pooling needed. This is how Neon works natively in Vercel Edge Functions, Cloudflare Workers, or any fetch-based runtime.
Neon is Postgres — which means it inherits the entire Postgres extension ecosystem. "Blobs" in Neon are handled natively through standard SQL types and the Pageserver's object storage backend.
BYTEA column type — raw binary in-row storagepg_largeobject catalogpg_s3 / foreign data wrappersNeon doesn't have a separate "functions" product. Your logic lives in Postgres itself via PL/pgSQL, PL/Python, or PL/v8 (JavaScript). You define stored procedures, triggers, and custom aggregates directly in SQL. This is powerful for data-adjacent logic (validation, auto-computed columns, audit trails) but these are not HTTP-accessible serverless functions — they're database-internal.
-- A stored function that runs inside Neon Postgres
CREATE OR REPLACE FUNCTION calculate_user_tier(user_id UUID)
RETURNS TEXT AS $$
DECLARE
total_spend NUMERIC;
BEGIN
SELECT SUM(amount) INTO total_spend
FROM orders
WHERE orders.user_id = $1
AND created_at > NOW() - INTERVAL '30 days';
RETURN CASE
WHEN total_spend > 1000 THEN 'gold'
WHEN total_spend > 100 THEN 'silver'
ELSE 'bronze'
END;
END;
$$ LANGUAGE plpgsql;
-- Call it like any SQL function
SELECT calculate_user_tier('abc-123-def');
This is Neon's flagship feature. Creating a branch is instantaneous — it's a metadata operation, not a data copy. Your CI/CD pipeline can spin up a full production-data branch for every PR, run migrations, run tests, and tear it down. No more "test against staging which diverged 3 months ago." Branch from any point in time. Merge is manual (apply migrations back to main).
// Works in Cloudflare Workers, Vercel Edge, Deno Deploy
// No persistent connection needed
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
// Single round-trip HTTP query
const users = await sql`
SELECT id, name, email
FROM users
WHERE active = true
LIMIT 10
`;
// Works as tagged template literal — SQL injection safe
Because Neon is real Postgres, every existing Postgres tool works: Prisma, Drizzle, SQLAlchemy, ActiveRecord, pg_dump, pg_restore, pgAdmin, DataGrip, Postico. Zero migration cost from existing Postgres stacks. You don't learn a new query language. You don't change your ORM. You just change your connection string.
Cloudflare's data products weren't planned from day one — they grew organically from the need to give Workers (their edge compute) somewhere to put state. The result is a portfolio of purpose-built primitives, each optimized for a specific access pattern.
┌─────────────────────┐
│ CLIENT REQUEST │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ CLOUDFLARE NETWORK │
│ 300+ PoPs global │
│ Anycast routing │
└──────┬───────┬──────┘
│ │
┌─────────────▼─┐ ┌─▼──────────────┐
│ WORKERS │ │ WORKERS │
│ (V8 Isolate) │ │ (V8 Isolate) │
│ Tokyo PoP │ │ Frankfurt PoP │
└──┬─────┬──────┘ └──┬──────┬───────┘
│ │ │ │
┌──────▼─┐ ┌─▼───────┐ ┌──▼──┐ ┌─▼────────┐
│ KV │ │Durable │ │ D1 │ │ R2 │
│ Store │ │Objects │ │(SQL)│ │ (Blobs) │
└──────┬─┘ └─────────┘ └──┬──┘ └──────────┘
│ │
┌──────▼───────────────────▼──────────┐
│ CLOUDFLARE GLOBAL STORAGE │
│ Replicated across regions/PoPs │
│ Consistency model varies by product │
└─────────────────────────────────────┘
Everything runs inside Workers — V8 isolates (not containers, not VMs) that start in under 1ms. Workers are the execution environment that calls your data primitives. The crucial architectural insight: your compute is already at the edge, so your data access needs to be too. High-latency database calls to a centralized server kill the edge performance advantage.
The most architecturally interesting Cloudflare product. A Durable Object is a single-instance JavaScript class that lives at one location in the world and serializes all access to its state. It solves distributed coordination without distributed locks. Think: a shopping cart, a game room, a real-time document — anything that needs one authoritative owner. State is transactionally consistent within the object; all writes go to one place.
KV is Cloudflare's eventually-consistent global cache. Write once, replicate everywhere. Reads return in <1ms from any PoP because data is cached at every edge node. Writes propagate globally within ~60 seconds. Perfect for: feature flags, session tokens, user preferences, public config. Not perfect for: anything that needs strong consistency or frequent writes.
D1 is Cloudflare's relational database product. It's SQLite-compatible, meaning you use standard SQL — but SQLite, not Postgres. D1 co-locates with Workers as SQLite-backed SQL. Cloudflare has shipped global read replication (Sessions API; async replicas with possible lag)—writes still go through a single primary, and each database remains size/throughput constrained relative to full Postgres. D1 is excellent for Workers-native, read-heavy, moderate-scale apps; Neon remains the clearer fit when you need the Postgres dialect, extensions (e.g. pgvector), branching, or large transactional SaaS schemas.
R2 is Cloudflare's S3-compatible object storage. The key differentiator: zero egress fees. With S3, you pay to read your own data out. With R2, egress is free. Store images, videos, backups, ML model weights, CSVs. Access from Workers at very low latency. R2 has an S3-compatible API so your existing tooling works immediately.
PutObject / GetObject// wrangler.toml: [[r2_buckets]]
// binding = "MY_BUCKET", bucket_name = "uploads"
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (request.method === 'PUT') {
// Store blob directly from request body
await env.MY_BUCKET.put(key, request.body, {
httpMetadata: { contentType: request.headers.get('content-type') }
});
return new Response('Uploaded', { status: 200 });
}
// Retrieve blob — served from edge cache
const obj = await env.MY_BUCKET.get(key);
if (!obj) return new Response('Not Found', { status: 404 });
return new Response(obj.body, {
headers: { 'content-type': obj.httpMetadata?.contentType ?? 'application/octet-stream' }
});
}
};
KV is a write-once, read-everywhere store. Values up to 25MB per key. Ideal for data that changes infrequently but needs to be read millions of times per second globally. The eventual consistency model (60s propagation) is a design constraint to work around, not against.
// Write from your CI/CD pipeline
await env.FLAGS.put('dark_mode_rollout', JSON.stringify({
enabled: true,
percentage: 25,
updated: Date.now()
}), { expirationTtl: 3600 }); // Auto-expire in 1 hour
// Read at every edge PoP — sub-millisecond
const flag = JSON.parse(await env.FLAGS.get('dark_mode_rollout'));
if (flag?.enabled && Math.random() < flag.percentage / 100) {
// Serve dark mode variant
}
Cloudflare Queues is a durable, at-least-once message queue for Workers. Enqueue work from one Worker, process it in a consumer Worker. Dead letter queues, retry logic, and batch processing built in. Think: email sending, webhook processing, background jobs — without managing Redis or SQS.
Cloudflare runs inference workloads (text generation, embeddings, image classification) at edge PoPs. You call models like LLaMA or Whisper from a Worker with a simple binding — no external API calls, no cold starts on GPU instances. The data never leaves the edge layer.
The edge network itself. When your Worker runs in Tokyo, your KV read, your R2 fetch, your D1 query — all of it is local to that PoP. The physics advantage is real: a centralized Postgres instance in us-east-1 will always add 150ms+ latency for a Tokyo user. Cloudflare's answer is to bring the database to the user.
| Dimension | 🌿 Neon | 🔶 Cloudflare |
|---|---|---|
| Database Engine | Full PostgreSQL 16 | SQLite (D1) / Custom (KV, DO) |
| Query Language | Standard SQL + all PG syntax | SQLite SQL dialect (D1) |
| Compute Location | Regional (AWS us-east, eu-west, etc.) | 300+ edge PoPs globally |
| Cold Start | few hundred ms (compute wake) / instant if warm | <1ms (V8 isolate) |
| Scale to Zero | ✓ Native, automatic | ✓ Always (Workers stateless) |
| Database Branching | ✓ Instant, copy-on-write | ✗ Not available |
| Point-in-Time Recovery | ✓ Any second in window | ~ D1 has backup/restore |
| Object / Blob Storage | ~ Via BYTEA / external FDW | ✓ R2 (S3-compatible, zero egress) |
| Global KV / Cache | ✗ Not native | ✓ Workers KV (eventual consistency) |
| Consistency Model | Strong (ACID Postgres) | Strong (D1 primary) / Eventual (KV, replicas) |
| Extensions Ecosystem | ✓ Full Postgres extensions | ✗ No extension model |
| Message Queue | ✗ Not native | ✓ Cloudflare Queues |
| Serverless Functions | ~ Postgres stored procedures | ✓ Workers (full JS/TS/Rust runtime) |
| Read Latency (local) | 1–5ms (warm, same region) | <1ms (edge KV / D1 replica) |
| Complex Joins | ✓ Full query planner | ~ D1 supports joins, limited optimizer |
| Connection Protocol | pg wire + HTTP-over-SQL | Native Workers bindings (no TCP) |
| ORM Compatibility | ✓ Any Postgres ORM | ~ Drizzle, D1-specific adapters |
| ML / Vector Search | ✓ pgvector extension | ✓ Workers AI + Vectorize |
| Free Tier | Generous (compute hours + 0.5GB) | Very generous (Workers, R2, D1 free tiers) |
| Egress Pricing | Standard cloud egress | Zero egress (R2, KV) |
| Multi-tenant Support | ✓ Branches per tenant | ✓ Durable Objects per tenant |
D1 uses SQLite syntax. If you're migrating from Postgres, you'll hit dialect differences: no JSONB aggregation, limited window functions, no ARRAY type, no custom aggregates, no pg_cron, no PostGIS. For new projects this is fine; for Postgres migrations, Neon wins hands-down.
Neon explicitly supports being called from Cloudflare Workers via its HTTP-over-SQL driver. Your Worker handles the edge routing, auth, caching (KV), and blob serving (R2). For anything requiring relational queries, it fires an HTTP request to Neon and gets back SQL results. You get edge performance for cached/simple reads and full Postgres power for complex queries. No compromises.
// Cloudflare Worker using Neon's HTTP driver
import { neon } from '@neondatabase/serverless';
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 1. Check KV cache first (sub-ms from edge)
const cached = await env.CACHE.get(url.pathname);
if (cached) return new Response(cached, { headers: { 'x-cache': 'HIT' } });
// 2. Query Neon Postgres via HTTP (no TCP connection)
const sql = neon(env.DATABASE_URL);
const data = await sql`
SELECT p.*, u.name as author_name
FROM posts p JOIN users u ON p.user_id = u.id
WHERE p.slug = ${url.pathname.slice(1)}
AND p.published = true
`;
if (!data.length) return new Response('Not Found', { status: 404 });
// 3. Cache result in KV for 5 minutes
const result = JSON.stringify(data[0]);
await env.CACHE.put(url.pathname, result, { expirationTtl: 300 });
return new Response(result, {
headers: { 'content-type': 'application/json', 'x-cache': 'MISS' }
});
}
};