Neon Postgres: Serverless Database Guide
Neon brings the power of PostgreSQL to the serverless world, letting you spin up a fully managed, autoscaling database with zero‑ops provisioning. In this guide we’ll walk through Neon’s core concepts, set up a Python project, and explore real‑world patterns that make serverless PostgreSQL a first‑class citizen in modern applications.
What is Neon?
Neon is a cloud‑native Postgres offering that decouples compute from storage, enabling you to pause, scale, or restart the compute layer without affecting the underlying data. Think of it as “PostgreSQL on demand” – you pay only for the compute cycles you actually use, while the data lives on durable object storage.
The platform is built on top of the open‑source PostgreSQL engine, so all familiar extensions, SQL syntax, and tooling work out of the box. This means you can migrate existing workloads with minimal friction and still benefit from Neon’s serverless features.
Serverless Architecture
Neon splits the database into three logical components: a storage tier (object storage), a compute tier (stateless PostgreSQL instances), and an API gateway that routes client connections. When traffic spikes, Neon spins up additional compute instances; when idle, it can suspend them, reducing costs dramatically.
Pricing Model
Neon charges for three things: storage (GB‑months), compute usage (CPU‑seconds), and data egress. There’s a generous free tier that includes 10 GB of storage and 20 M compute seconds per month – perfect for prototyping or low‑traffic apps.
Getting Started
The first step is to create a Neon account at neon.tech. After confirming your email, you’ll land on the dashboard where you can create a new project. Give it a name, select a region close to your users, and hit “Create”.
Neon will generate a connection string that looks like postgres://username:password@project-id.neon.tech:5432/database. This URL contains everything you need – host, port, database name, and authentication credentials – and can be used directly with any PostgreSQL driver.
Configuring Environment Variables
For security, store the connection string in an environment variable, e.g. NEON_URL. In a local development setup you can place it in a .env file and load it with python-dotenv. This keeps secrets out of your source code and makes it easy to switch between Neon and a local Postgres instance.
Connecting from Python
Python’s ecosystem offers several PostgreSQL drivers. For synchronous code, psycopg2-binary works flawlessly; for async workloads, asyncpg is the go‑to choice. Below we’ll demonstrate both.
Basic Synchronous Connection
import os
import psycopg2
from psycopg2.extras import RealDictCursor
# Load connection string from environment
NEON_URL = os.getenv("NEON_URL")
# Establish a connection
with psycopg2.connect(NEON_URL, cursor_factory=RealDictCursor) as conn:
with conn.cursor() as cur:
cur.execute("SELECT version();")
result = cur.fetchone()
print("PostgreSQL version:", result["version"])
This snippet opens a connection, runs a simple query, and prints the server version. Because Neon’s compute layer can pause, the driver automatically reconnects if the connection is dropped, so you rarely need to handle reconnection logic manually.
Asynchronous Connection Pooling with asyncpg
Serverless workloads often run in an async event loop (e.g., FastAPI or Quart). Using a connection pool minimizes latency and respects Neon’s connection limits.
import os
import asyncio
import asyncpg
NEON_URL = os.getenv("NEON_URL")
async def fetch_users():
# Create a pool with a max of 5 connections
pool = await asyncpg.create_pool(dsn=NEON_URL, max_size=5)
async with pool.acquire() as conn:
rows = await conn.fetch(
"SELECT id, email FROM users ORDER BY created_at DESC LIMIT 10;"
)
for row in rows:
print(row["id"], row["email"])
await pool.close()
if __name__ == "__main__":
asyncio.run(fetch_users())
Notice the use of create_pool – Neon automatically scales the underlying compute nodes to satisfy the pool’s demand, then scales them down when idle.
Managing Schemas & Multi‑Tenancy
One of Neon’s strengths is its ability to spin up isolated compute instances per tenant while sharing the same storage. This is achieved by using separate PostgreSQL schemas for each tenant.
Creating a Tenant Schema
def create_tenant_schema(conn, tenant_id: str):
with conn.cursor() as cur:
# Sanitize identifier to avoid SQL injection
schema_name = f"tenant_{tenant_id}"
cur.execute(f"CREATE SCHEMA IF NOT EXISTS {schema_name};")
# Grant usage to the application role
cur.execute(
f"GRANT USAGE ON SCHEMA {schema_name} TO app_user;"
)
# Optionally set a default search_path for the tenant
cur.execute(
f"ALTER ROLE app_user SET search_path = {schema_name}, public;"
)
After creating the schema, you can route all tenant‑specific queries to it by adjusting the search_path. This isolates data without needing separate databases, which keeps storage costs low.
Serverless Patterns with Neon
Because Neon’s compute is stateless, you can adopt patterns that would be risky on a traditional VM‑based database. Below are three common approaches.
- Cold‑Start Mitigation: Warm up a compute node by issuing a lightweight query (e.g.,
SELECT 1) on a schedule. - Auto‑Scaling Queries: Use connection pools that grow on demand; Neon will provision additional compute instances automatically.
- Background Workers: Run periodic jobs (e.g., data aggregation) as serverless functions that connect to Neon on demand, paying only for the execution time.
Real‑World Use Cases
Neon’s serverless model shines in scenarios where traffic is unpredictable or where you want to reduce operational overhead.
- SaaS Multi‑Tenant Platforms: Store each customer’s data in a dedicated schema, spin up compute only when a tenant is active, and pause it during inactivity.
- Event‑Driven ETL Pipelines: Trigger a serverless function (e.g., AWS Lambda) on a new S3 object, load the data into Neon, and let Neon’s compute scale for the brief load.
- CI/CD Test Environments: Spin up a fresh Neon branch for each pull request, run integration tests, then discard the branch – all without provisioning a full database server.
Best Practices & Pro Tips
Tip 1 – Keep Connections Short‑Lived: Neon caps the number of concurrent connections per compute node. Use connection pooling libraries and close connections promptly to avoid hitting limits.
Tip 2 – Leverage Read‑Only Replicas: For heavy read workloads, enable Neon’s read‑only endpoint. It runs on a separate compute pool, leaving write capacity free for transactional traffic.
Tip 3 – Monitor Cold Starts: If your latency budget is tight, schedule a “heartbeat” query every few minutes to keep at least one compute node warm.
Tip 4 – Use Parameterized Queries: Always prefer
$1, $2placeholders over string interpolation to protect against SQL injection and let PostgreSQL cache execution plans.
Monitoring & Observability
Neon provides a built-in console that shows CPU usage, connection count, and query latency in real time. For deeper insight, you can query PostgreSQL’s statistics tables.
def print_active_queries(conn):
with conn.cursor() as cur:
cur.execute("""
SELECT pid, state, query_start, now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;
""")
for row in cur.fetchall():
print(f"PID {row[0]} running for {row[3]}: {row[4][:80]}...")
Integrate this monitoring script with your alerting system to catch runaway queries before they consume all compute resources.
Migration & CI/CD Integration
Because Neon speaks standard PostgreSQL, you can use any migration tool you’re comfortable with – Alembic, Flyway, or Prisma. Below is a minimal Alembic configuration that points to a Neon URL.
# alembic.ini snippet
[alembic]
script_location = migrations
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
# env.py snippet
from alembic import context
import os
config = context.config
config.set_main_option('sqlalchemy.url', os.getenv('NEON_URL'))
target_metadata = None
...
Run alembic upgrade head in your CI pipeline to keep the Neon schema in sync with your codebase. Because Neon’s storage is durable, you can safely roll back migrations without data loss.
Security Considerations
Neon supports role‑based access control (RBAC) just like any PostgreSQL instance. Create a dedicated role for your application, grant only the required privileges, and rotate its password regularly.
CREATE ROLE app_user LOGIN PASSWORD 'strong‑random‑pwd';
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
Network‑level security is handled by Neon’s managed firewall. You can restrict access to specific IP ranges or VPC endpoints, ensuring that only your trusted services can talk to the database.
Conclusion
Neon transforms PostgreSQL into a truly serverless data platform, giving developers the flexibility to scale compute on demand while keeping storage cheap and durable. By following the patterns outlined above – from quick connection setup to multi‑tenant schema design and robust monitoring – you can build production‑grade applications that reap the cost and operational benefits of a serverless database.