Neon, serverless PostgreSQL z rozgałęzianiem bazy
Neon to hostowany PostgreSQL zbudowany wokół dwóch pomysłów: obliczenia są oddzielone od danych, więc baza może zejść do zera przy braku ruchu, a kopię całej bazy tworzy się w sekundę bez kopiowania danych. To drugie nazywa się rozgałęzianiem i działa podobnie jak gałąź w systemie kontroli wersji. Silnik jest otwarty na licencji Apache 2.0.
Przejęcie przez Databricks
Zacznę od zmiany właścicielskiej, bo starsze materiały jej nie uwzględniają, a ma wpływ na to, dokąd ten produkt zmierza.
14 maja 2025 roku Databricks ogłosił przejęcie Neona w transakcji szacowanej na około miliard dolarów. Uzasadnienie podane przy tej okazji jest ciekawsze niż sama kwota: kupujący podał, że mniej więcej osiemdziesiąt procent baz zakładanych w tym serwisie tworzyły agenty AI, a nie ludzie. To wyjaśnia, dlaczego akurat ten produkt był wart takich pieniędzy dla firmy z obszaru danych.
Ta liczba przestaje dziwić, gdy spojrzysz, skąd te bazy się biorą. W narzędziach takich jak Replit, gdzie agent buduje aplikację z opisu, uruchamia ją i wdraża, podłączenie bazy nie jest osobnym krokiem wykonywanym przez człowieka, tylko elementem generowanego projektu. Baza zakładana automatycznie, odpytywana rzadko i stojąca bezczynnie między jednym poleceniem a drugim to dokładnie ten kształt obciążenia, pod który pasuje rozliczenie za użycie i usypianie bezczynnej instancji.
Konsekwencje dla użytkownika okazały się korzystne, przynajmniej cenowo. Od sierpnia 2025 roku obowiązuje nowy cennik, w którym koszt obliczeń spadł o kilkanaście do dwudziestu kilku procent, a koszt przechowywania danych z 1,75 do 0,35 dolara za gigabajt miesięcznie. W październiku 2025 podwojono darmowy przydział obliczeń, a w grudniu zniesiono minimalne opłaty miesięczne, przez co plany płatne stały się w całości rozliczane za użycie.
Warto natomiast zachować zdrową ostrożność przy planowaniu długoterminowym. Produkt przejęty przez większą firmę zwykle prędzej czy później zaczyna być kształtowany pod jej strategię, a strategia Databricks dotyczy platformy danych, nie hostingu baz dla aplikacji webowych. Na razie kierunek jest korzystny, ale to nie jest gwarancja na lata.
Rozgałęzianie, czyli główna cecha wyróżniająca
To funkcja, dla której ludzie wybierają tę bazę zamiast dowolnej innej, i warto zrozumieć, dlaczego działa szybko, bo z tego wynikają zarówno możliwości, jak i ograniczenia.
Gałąź nie jest kopią danych. Powstaje jako wskaźnik na stan magazynu w danym momencie, a nowe zapisy trafiają do warstwy różnicowej. Dlatego utworzenie gałęzi z bazy o rozmiarze stu gigabajtów trwa tyle samo, co z bazy o rozmiarze stu megabajtów, i nie podwaja rachunku za przechowywanie. Płacisz dopiero za to, co się w tej gałęzi zmieni.
Praktyczne zastosowania wynikają z tego same. Każde zgłoszenie zmian może dostać własną bazę z pełną kopią danych produkcyjnych, przez co migracje testuje się na realnym zbiorze, a nie na wydmuszce z trzema rekordami. Środowisko przejściowe przestaje być osobnym systemem do utrzymania. Odtworzenie stanu sprzed awarii sprowadza się do utworzenia gałęzi z punktu w czasie, zamiast do przywracania kopii zapasowej.
Ograniczenie jest jedno i wynika wprost z konstrukcji: gałąź współdzieli magazyn z rodzicem, więc nie jest izolowana wydajnościowo w takim stopniu jak osobna instancja. Do testów obciążeniowych, gdzie chodzi o zmierzenie wydajności, to nie jest właściwe narzędzie.
Skalowanie do zera i jego cena
Druga cecha, która przyciąga, to zejście do zera przy braku ruchu. Baza zasypia po okresie bezczynności i przestaje generować koszt obliczeń, a budzi się przy pierwszym zapytaniu.
Cena tej wygody nazywa się zimnym startem. Pierwsze zapytanie po przebudzeniu trwa dłużej niż kolejne, i choć mówi się tu o setkach milisekund, w aplikacji odpowiadającej użytkownikowi to jest różnica odczuwalna. Dla projektu pobocznego, panelu wewnętrznego czy środowiska testowego kompromis jest oczywisty. Dla produktu, w którym pierwsze wejście użytkownika ma być szybkie, warto wyłączyć usypianie i pogodzić się ze stałym kosztem.
Jest tu też pułapka, która zaskakuje przy szacowaniu rachunku. Skalowanie do zera działa tylko wtedy, gdy naprawdę nic nie odpytuje bazy. Monitorowanie sprawdzające dostępność co minutę, zadanie w tle odświeżające cokolwiek albo pula połączeń trzymająca otwarte połączenie skutecznie utrzymują bazę w stanie czuwania przez całą dobę. Jeśli rachunek za obliczenia wygląda podejrzanie wysoko przy niskim ruchu, tego właśnie szukaj w pierwszej kolejności.
Dlaczego Neon?
Kluczowe zalety
- Branching bazy danych - Twórz kopie bazy w sekundy, bez kopiowania danych
- Scale to zero - Brak opłat gdy aplikacja nie jest używana
- Instant wake - Cold start poniżej 500ms
- Pay per use - Płać tylko za faktyczne compute hours
- Pełna kompatybilność z PostgreSQL - Wsparcie dla pgvector, PostGIS, i innych rozszerzeń
- Bezpieczne połączenie - Wszystkie połączenia szyfrowane TLS
Neon vs tradycyjne bazy danych
| Cecha | Neon | RDS/Cloud SQL | Supabase |
|---|---|---|---|
| Scale to zero | Tak | Nie | Nie |
| Branching | Natywne | Migawki | Nie |
| Cold start | ~500ms | Brak | Brak |
| Pricing model | Per compute | Per hour | Flat + usage |
| Free tier storage | 512MB | Brak | 500MB |
| Connection pooling | Wbudowane | Dodatkowe | Wbudowane |
Kiedy wybrać Neon?
- Side projects i MVP - Darmowy plan z scale to zero
- Preview environments - Osobna baza dla każdego PR
- CI/CD testing - Izolowane środowiska testowe
- Serverless backends - Idealnie współpracuje z Edge Functions
- Development workflow - Branching dla feature development
Kiedy rozważyć alternatywy?
- Stałe, wysokie obciążenie - Dedicated instance może być tańszy
- Bardzo niskie latency - Własny serwer może być szybszy
- Specifyczne rozszerzenia PG - Sprawdź dostępność w Neon
Architektura Neon
Separacja Compute i Storage
┌─────────────────────────────────────────────────────────────┐
│ Neon Cloud │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Compute │ │ Compute │ │ Compute │ │
│ │ (main) │ │ (dev) │ │ (pr-123) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────────────┼───────────────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Pageserver │ │
│ │ (Storage) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Safekeepers │ │
│ │ (WAL/Durability) │
│ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Jak działa branching?
main branch
│
├── commit 1
│
├── commit 2
│ │
│ └──► dev branch (copy-on-write)
│ │
│ ├── dev commit 1
│ │
│ └── dev commit 2
│
├── commit 3
│ │
│ └──► pr-123 branch
│
└── commit 4Branch w Neon to copy-on-write snapshot - nie kopiuje danych fizycznie, tylko metadane. Dlatego tworzenie brancha trwa sekundy, niezależnie od rozmiaru bazy.
Rozpoczęcie pracy
Tworzenie projektu
- Zarejestruj się na console.neon.tech
- Stwórz nowy projekt
- Wybierz region (dostępne: US East, US West, Europe, Asia)
- Skopiuj connection string
Connection String
postgresql://[user]:[password]@[host]/[database]?sslmode=requirePrzykład:
postgresql://neonuser:password123@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=requireInstalacja CLI
# npm
npm install -g neonctl
# Homebrew (macOS)
brew install neonctl
# Autoryzacja
neonctl authBranching - Git dla bazy danych
Tworzenie branchy przez CLI
# Lista branchy
neonctl branches list
# Utwórz branch z main
neonctl branches create --name dev
# Utwórz branch z określonego punktu w czasie
neonctl branches create --name staging --parent main --point-in-time "2024-01-15T10:00:00Z"
# Utwórz branch dla PR
neonctl branches create --name pr-456 --parent main
# Usuń branch
neonctl branches delete pr-456Branching w CI/CD
# .github/workflows/preview.yml
name: Preview Environment
on:
pull_request:
types: [opened, synchronize]
jobs:
create-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Neon Branch
id: create-branch
uses: neondatabase/create-branch-action@v5
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ github.event.pull_request.number }}
api_key: ${{ secrets.NEON_API_KEY }}
- name: Run Migrations
run: |
DATABASE_URL="${{ steps.create-branch.outputs.db_url }}" npm run migrate
- name: Deploy Preview
run: |
# Deploy z nowym DATABASE_URL
vercel --env DATABASE_URL="${{ steps.create-branch.outputs.db_url }}"Automatyczne usuwanie branchy
# .github/workflows/cleanup.yml
name: Cleanup Preview
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Delete Neon Branch
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ github.event.pull_request.number }}
api_key: ${{ secrets.NEON_API_KEY }}Łączenie z aplikacją
Neon Serverless Driver
Oficjalny driver zoptymalizowany dla serverless:
npm install @neondatabase/serverlessimport { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
// Proste zapytanie
const users = await sql`SELECT * FROM users WHERE active = true`
// Z parametrami
const userId = 1
const user = await sql`SELECT * FROM users WHERE id = ${userId}`
// Insert
const newUser = await sql`
INSERT INTO users (name, email)
VALUES (${'John Doe'}, ${'john@example.com'})
RETURNING *
`
// Transaction (pojedyncza)
const result = await sql`
WITH inserted AS (
INSERT INTO orders (user_id, total)
VALUES (${userId}, ${99.99})
RETURNING id
)
INSERT INTO order_items (order_id, product_id, quantity)
SELECT id, ${productId}, ${quantity} FROM inserted
RETURNING *
`Neon z Pool (dla długich połączeń)
import { Pool } from '@neondatabase/serverless'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
// Użycie z pool
const client = await pool.connect()
try {
await client.query('BEGIN')
await client.query('INSERT INTO orders ...', [values])
await client.query('UPDATE inventory ...', [values])
await client.query('COMMIT')
} catch (e) {
await client.query('ROLLBACK')
throw e
} finally {
client.release()
}
// Zamknięcie pool przy shutdown
await pool.end()WebSocket dla real-time
import { neon, neonConfig } from '@neondatabase/serverless'
import ws from 'ws'
// Dla środowisk Node.js (Vercel Edge, Cloudflare Workers mają wbudowane WS)
neonConfig.webSocketConstructor = ws
const sql = neon(process.env.DATABASE_URL!)Integracja z ORM
Drizzle ORM
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit// db/schema.ts
import { pgTable, serial, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
isActive: boolean('is_active').default(true),
})
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
publishedAt: timestamp('published_at'),
})// db/index.ts
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
import * as schema from './schema'
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })
// Użycie
const allUsers = await db.select().from(schema.users)
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
})
// Insert
await db.insert(schema.users).values({
name: 'Jan Kowalski',
email: 'jan@example.com',
})
// Update
await db.update(schema.users)
.set({ isActive: false })
.where(eq(schema.users.id, 1))
// Delete
await db.delete(schema.users).where(eq(schema.users.id, 1))// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config# Generuj migracje
npx drizzle-kit generate:pg
# Uruchom migracje
npx drizzle-kit push:pg
# Studio (GUI)
npx drizzle-kit studioPrisma
npm install prisma @prisma/client
npx prisma init// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
isActive Boolean @default(true)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
author User @relation(fields: [authorId], references: [id])
authorId Int
publishedAt DateTime?
}// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaNeon } from '@prisma/adapter-neon'
import { Pool } from '@neondatabase/serverless'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaNeon(pool)
export const prisma = new PrismaClient({ adapter })
// Użycie
const users = await prisma.user.findMany({
include: { posts: true },
})
await prisma.user.create({
data: {
name: 'Jan Kowalski',
email: 'jan@example.com',
posts: {
create: {
title: 'Pierwszy post',
content: 'Treść postu...',
},
},
},
})# Migracje
npx prisma migrate dev --name init
npx prisma migrate deploy
# Studio
npx prisma studioKysely
npm install kysely @neondatabase/serverless// db/types.ts
import { Generated, Insertable, Selectable, Updateable } from 'kysely'
export interface Database {
users: UsersTable
posts: PostsTable
}
interface UsersTable {
id: Generated<number>
name: string
email: string
created_at: Generated<Date>
is_active: Generated<boolean>
}
interface PostsTable {
id: Generated<number>
title: string
content: string | null
author_id: number
published_at: Date | null
}
export type User = Selectable<UsersTable>
export type NewUser = Insertable<UsersTable>
export type UserUpdate = Updateable<UsersTable>// db/index.ts
import { Kysely } from 'kysely'
import { NeonDialect } from 'kysely-neon'
import { Database } from './types'
export const db = new Kysely<Database>({
dialect: new NeonDialect({
connectionString: process.env.DATABASE_URL!,
}),
})
// Użycie
const users = await db
.selectFrom('users')
.selectAll()
.where('is_active', '=', true)
.execute()
await db
.insertInto('users')
.values({ name: 'Jan', email: 'jan@example.com' })
.execute()Integracja z Next.js
App Router z Server Actions
// app/actions/users.ts
'use server'
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { revalidatePath } from 'next/cache'
export async function getUsers() {
return db.select().from(users)
}
export async function createUser(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
await db.insert(users).values({ name, email })
revalidatePath('/users')
}
export async function deleteUser(id: number) {
await db.delete(users).where(eq(users.id, id))
revalidatePath('/users')
}// app/users/page.tsx
import { getUsers } from '@/app/actions/users'
import { UserForm } from './user-form'
import { UserList } from './user-list'
export default async function UsersPage() {
const users = await getUsers()
return (
<div>
<h1>Users</h1>
<UserForm />
<UserList users={users} />
</div>
)
}// app/users/user-form.tsx
'use client'
import { createUser } from '@/app/actions/users'
export function UserForm() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Add User</button>
</form>
)
}API Routes
// app/api/users/route.ts
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
export async function GET() {
try {
const result = await db.select().from(users)
return NextResponse.json(result)
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
)
}
}
export async function POST(request: Request) {
try {
const body = await request.json()
const { name, email } = body
const result = await db
.insert(users)
.values({ name, email })
.returning()
return NextResponse.json(result[0], { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
)
}
}// app/api/users/[id]/route.ts
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const user = await db
.select()
.from(users)
.where(eq(users.id, parseInt(params.id)))
.limit(1)
if (!user.length) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json(user[0])
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const result = await db
.update(users)
.set(body)
.where(eq(users.id, parseInt(params.id)))
.returning()
return NextResponse.json(result[0])
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.delete(users).where(eq(users.id, parseInt(params.id)))
return new NextResponse(null, { status: 204 })
}Edge Functions (Vercel, Cloudflare)
Vercel Edge Functions
// app/api/edge/route.ts
import { neon } from '@neondatabase/serverless'
export const runtime = 'edge'
export async function GET() {
const sql = neon(process.env.DATABASE_URL!)
const users = await sql`SELECT * FROM users LIMIT 10`
return Response.json(users)
}Cloudflare Workers
// src/index.ts
import { neon } from '@neondatabase/serverless'
export interface Env {
DATABASE_URL: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sql = neon(env.DATABASE_URL)
const url = new URL(request.url)
if (url.pathname === '/users') {
const users = await sql`SELECT * FROM users`
return Response.json(users)
}
return new Response('Not Found', { status: 404 })
},
}# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
# Ustaw w Cloudflare Dashboard jako secret
# DATABASE_URL = "..."pgvector - Embeddingi i AI
Włączenie pgvector
-- W Neon Console lub przez SQL
CREATE EXTENSION IF NOT EXISTS vector;Schema z wektorami
// db/schema.ts
import { pgTable, serial, text, vector } from 'drizzle-orm/pg-core'
export const documents = pgTable('documents', {
id: serial('id').primaryKey(),
content: text('content').notNull(),
embedding: vector('embedding', { dimensions: 1536 }), // OpenAI ada-002
})Semantic Search
import { db } from '@/db'
import { documents } from '@/db/schema'
import { sql } from 'drizzle-orm'
import OpenAI from 'openai'
const openai = new OpenAI()
async function getEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: text,
})
return response.data[0].embedding
}
export async function semanticSearch(query: string, limit = 5) {
const queryEmbedding = await getEmbedding(query)
// Drizzle z raw SQL dla operacji wektorowych
const results = await db.execute(sql`
SELECT
id,
content,
1 - (embedding <=> ${queryEmbedding}::vector) as similarity
FROM documents
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${limit}
`)
return results.rows
}
// Dodawanie dokumentu z embeddingiem
export async function addDocument(content: string) {
const embedding = await getEmbedding(content)
await db.insert(documents).values({
content,
embedding,
})
}RAG Pipeline
// lib/rag.ts
import { semanticSearch, addDocument } from './vector-search'
import OpenAI from 'openai'
const openai = new OpenAI()
export async function askQuestion(question: string): Promise<string> {
// 1. Znajdź relevantne dokumenty
const relevantDocs = await semanticSearch(question, 3)
// 2. Zbuduj kontekst
const context = relevantDocs
.map(doc => doc.content)
.join('\n\n')
// 3. Generuj odpowiedź z LLM
const response = await openai.chat.completions.create({
model: 'gpt-4-turbo-preview',
messages: [
{
role: 'system',
content: `Odpowiadaj na pytania na podstawie podanego kontekstu.
Jeśli odpowiedź nie jest w kontekście, powiedz o tym.
Kontekst:
${context}`,
},
{
role: 'user',
content: question,
},
],
})
return response.choices[0].message.content || ''
}Point-in-Time Recovery
Przywracanie do punktu w czasie
# Przez CLI
neonctl branches create \
--name recovery-branch \
--parent main \
--point-in-time "2024-01-15T14:30:00Z"Przez API
const response = await fetch('https://console.neon.tech/api/v2/projects/{project_id}/branches', {
method: 'POST',
headers: {
'Authorization': `Bearer ${NEON_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
branch: {
name: 'recovery-branch',
parent_id: 'main',
},
endpoints: [
{
type: 'read_write',
},
],
// Przywróć do konkretnego momentu
parent_timestamp: '2024-01-15T14:30:00Z',
}),
})Connection Pooling
Neon oferuje wbudowany connection pooler (PgBouncer):
# Pooled connection (dla serverless)
postgresql://user:pass@ep-xxx.pooler.region.aws.neon.tech/dbname?sslmode=require
# Direct connection (dla migracji)
postgresql://user:pass@ep-xxx.region.aws.neon.tech/dbname?sslmode=requireKonfiguracja w aplikacji
// Dla serverless (większość przypadków)
const pooledUrl = process.env.DATABASE_URL // używaj pooled
// Dla migracji (potrzebujesz bezpośredniego połączenia)
const directUrl = process.env.DIRECT_DATABASE_URL# .env
DATABASE_URL="postgresql://user:pass@ep-xxx.pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"
DIRECT_DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"Monitoring i metryki
Neon Console
- Compute usage - Aktywne compute units
- Storage - Użycie dysku na branch
- Connections - Aktywne połączenia
- Query performance - Analiza zapytań
Własny monitoring
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
// Statystyki połączeń
const connectionStats = await sql`
SELECT
count(*) as total_connections,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle
FROM pg_stat_activity
`
// Rozmiar tabel
const tableSizes = await sql`
SELECT
relname as table_name,
pg_size_pretty(pg_total_relation_size(relid)) as total_size,
pg_size_pretty(pg_relation_size(relid)) as data_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
`
// Wolne zapytania
const slowQueries = await sql`
SELECT
query,
calls,
mean_exec_time,
total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10
`Bezpieczeństwo
Role i uprawnienia
-- Utwórz role dla aplikacji
CREATE ROLE app_user WITH LOGIN PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE neondb 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;
-- Read-only role dla analytics
CREATE ROLE analytics_user WITH LOGIN PASSWORD 'analytics_pass';
GRANT CONNECT ON DATABASE neondb TO analytics_user;
GRANT USAGE ON SCHEMA public TO analytics_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_user;Row Level Security
-- Włącz RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: użytkownik widzi tylko swoje posty
CREATE POLICY user_posts ON posts
FOR ALL
USING (author_id = current_setting('app.user_id')::int);
-- Policy: publiczne posty widoczne dla wszystkich
CREATE POLICY public_posts ON posts
FOR SELECT
USING (is_public = true);// Ustawianie kontekstu użytkownika
const sql = neon(process.env.DATABASE_URL!)
async function getUserPosts(userId: number) {
await sql`SELECT set_config('app.user_id', ${userId.toString()}, false)`
return sql`SELECT * FROM posts`
}Cennik
| Plan | Cena | Przechowywanie | Obliczenia | Projekty | Gałęzie na projekt |
|---|---|---|---|---|---|
| Free | 0 USD | 0,5 GB na projekt | 100 jednostkogodzin na projekt | 100 | 10 |
| Launch | za użycie | 0,35 USD za GB miesięcznie | 0,106 USD za jednostkogodzinę | 100 | 10 |
| Scale | za użycie | 0,35 USD za GB miesięcznie | 0,222 USD za jednostkogodzinę | 1000 | 25 |
Model rozliczeń zmienił się w 2025 roku i to zmiana na korzyść, więc starsze zestawienia z opłatami stałymi w rodzaju dziewiętnastu dolarów miesięcznie są nieaktualne. Plany płatne nie mają dziś minimalnej opłaty, więc przy zerowym użyciu płacisz zero.
Trzy rzeczy warto wiedzieć przy szacowaniu. Jednostka obliczeniowa odpowiada mniej więcej procesorowi z czterema gigabajtami pamięci, a rozliczenie idzie za czas jej działania, nie za czas istnienia bazy. Ruch wychodzący jest wliczony do pięciuset gigabajtów na projekt w planach płatnych, powyżej dziesięć centów za gigabajt. Plan darmowy nie jest okresem próbnym, tylko stałym poziomem bez karty płatniczej.
Różnica między planami płatnymi sprowadza się głównie do stawki za obliczenia i limitów ilościowych. Wyższa stawka w planie Scale idzie w parze z większą liczbą projektów, gałęzi i możliwością podniesienia limitów na życzenie, więc nie jest to prosta dopłata za to samo.
- Cena: Custom
Rozliczenie za zużycie
- Moc obliczeniowa: 0,106 USD za jednostkę na godzinę w planie Launch, 0,222 USD w planie Scale
- Magazyn: 0,35 USD za gigabajt miesięcznie
- Ruch wychodzący: 500 GB w cenie na projekt, powyżej 0,10 USD za gigabajt
- Uśpiona instancja: 0 USD, bo skalowanie schodzi do zera
- Dodatkowe gałęzie: 1,50 USD za gałąź miesięcznie, naliczane godzinowo
Jedna jednostka na godzinę to rozmiar instancji pomnożony przez czas jej działania, więc instancja o rozmiarze 0,25 pracująca cztery godziny zużywa jedną jednostkę. Plan darmowy daje 100 takich jednostek na projekt i pół gigabajta magazynu.
Dobre praktyki
1. Używaj branchingu dla development
# Każdy developer ma własny branch
neonctl branches create --name dev-john
neonctl branches create --name dev-anna
# Każdy PR ma własny branch
neonctl branches create --name pr-${PR_NUMBER}2. Connection pooling dla serverless
// Zawsze używaj pooled connection w serverless
const sql = neon(process.env.DATABASE_URL!) // pooled URL3. Optymalizuj cold starty
// Prewarming przez scheduled function
// Uruchamiaj co 4 minuty żeby utrzymać compute active
export async function warmup() {
const sql = neon(process.env.DATABASE_URL!)
await sql`SELECT 1`
}4. Indeksy dla performance
-- Indeksy na często używanych kolumnach
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_posts_author ON posts(author_id);
-- Partial index dla aktywnych rekordów
CREATE INDEX CONCURRENTLY idx_active_users ON users(id) WHERE is_active = true;
-- Index dla full-text search
CREATE INDEX CONCURRENTLY idx_posts_content_gin ON posts USING gin(to_tsvector('polish', content));5. Monitoruj i optymalizuj
-- Znajdź brakujące indeksy
SELECT
relname,
seq_scan,
idx_scan,
seq_scan - idx_scan as diff
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
ORDER BY diff DESC;
-- Analiza zapytań
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';Typowe problemy i rozwiązania
Cold start timeout
// Problem: Zapytanie timeout podczas cold start
// Rozwiązanie: Zwiększ timeout i dodaj retry
const sql = neon(process.env.DATABASE_URL!, {
fetchOptions: {
timeout: 10000, // 10 sekund
},
})
async function queryWithRetry(query: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await sql(query)
} catch (error) {
if (i === retries - 1) throw error
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
}Connection limit exceeded
// Problem: Too many connections
// Rozwiązanie: Używaj pooled connection string
// Zamiast: ep-xxx.us-east-2.aws.neon.tech
// Użyj: ep-xxx.pooler.us-east-2.aws.neon.techBranch sync issues
# Problem: Branch outdated względem main
# Rozwiązanie: Utwórz nowy branch
neonctl branches delete dev-old
neonctl branches create --name dev-new --parent mainFAQ - Najczęściej zadawane pytania
Czy Neon jest production-ready?
Tak. Neon jest używany przez tysiące firm w produkcji. Oferuje 99.95% SLA na planach Business.
Jak długo trwa cold start?
Typowo 300-500ms. Można go uniknąć przez scheduled warming lub płatne "always on" compute.
Czy mogę używać wszystkich rozszerzeń PostgreSQL?
Większość popularnych rozszerzeń jest dostępna: pgvector, PostGIS, pg_trgm, hstore, uuid-ossp. Pełna lista na dokumentacji Neon.
Jak migrować z RDS/Supabase?
- Eksportuj dane:
pg_dump - Stwórz projekt Neon
- Importuj:
psql < dump.sql - Zaktualizuj connection string w aplikacji
Czy dane są szyfrowane?
Tak, połączenia idą po TLS, a dane spoczywające są szyfrowane. Szczegóły dotyczące certyfikacji i zgodności warto sprawdzić bezpośrednio u dostawcy, bo zakres bywa różny w zależności od planu i regionu.
Czy Neon nadal jest niezależny?
Nie. Od maja 2025 roku należy do Databricks. Produkt działa dalej i dostał w międzyczasie obniżki cen, ale przy planowaniu na lata warto pamiętać, że kierunek rozwoju wyznacza teraz właściciel z innego obszaru rynku.
Pula połączeń, czyli najczęstsze potknięcie
Ten problem dotyczy każdego PostgreSQL uruchamianego pod funkcjami na żądanie i tutaj wraca ze zdwojoną siłą, bo baza domyślnie zasypia.
Mechanizm jest prosty. Każda instancja funkcji zestawia własne połączenie, a PostgreSQL ma twardy limit połączeń jednoczesnych. Przy stu równoległych wywołaniach limit kończy się szybciej, niż zdążysz to zauważyć w logach, a objawem jest błąd o wyczerpaniu połączeń pojawiający się wyłącznie przy większym ruchu.
Rozwiązaniem jest adres z pulą połączeń. Neon udostępnia dwa łańcuchy połączenia: zwykły i przechodzący przez pulę, rozpoznawalny po odpowiednim członie w nazwie hosta. W środowisku bezstanowym zawsze bierz ten drugi.
Jest jednak zastrzeżenie, o którym łatwo zapomnieć. Pula w trybie transakcyjnym nie obsługuje wszystkiego: przygotowane instrukcje, tymczasowe tabele i porady na poziomie sesji zachowują się inaczej albo nie działają. Migracje schematu i długie zadania administracyjne uruchamiaj przez połączenie bezpośrednie, a przez pulę puszczaj zwykły ruch aplikacji.
Osobna ścieżka to sterownik komunikujący się po HTTP, sensowny w środowiskach brzegowych, gdzie gniazd TCP w ogóle nie da się otworzyć. Dostajesz wtedy pojedyncze zapytania bez trwałej sesji, co wyklucza transakcje wieloetapowe, ale rozwiązuje problem połączeń całkowicie.
Typowe błędy
Pierwszy to trzymanie jednego połączenia w zmiennej globalnej modułu. W środowisku, które usypia i budzi instancje, takie połączenie bywa martwe przy kolejnym wywołaniu, a błąd wygląda na przypadkowy, bo pojawia się tylko po dłuższej przerwie.
Drugi to zapominanie o usuwaniu gałęzi. Tworzy się je jednym poleceniem i łatwo zostawić po zamkniętym zgłoszeniu zmian. Same wskaźniki kosztują niewiele, ale różnice, które w nich narosły, już nie, a po pół roku nikt nie wie, które z pięćdziesięciu gałęzi są do czegoś potrzebne. Warto usuwać je automatycznie razem z zamknięciem zgłoszenia.
Trzeci to zakładanie, że skalowanie do zera zadziała samo. Jak wspomniałem wyżej, wystarczy jedno regularne odpytanie, żeby baza nigdy nie zasnęła. Sprawdź, co odpytuje bazę poza aplikacją, zanim zaczniesz szukać oszczędności gdzie indziej.
Czwarty to traktowanie gałęzi jako środowiska wydajnościowego. Współdzielą magazyn z rodzicem, więc pomiary czasu zapytań na gałęzi nie odpowiadają temu, co zobaczysz na produkcji pod obciążeniem.
Piąty to pomijanie kosztu ruchu wychodzącego przy dużych zbiorach. Pobieranie milionów wierszy do przetworzenia poza bazą wygląda niewinnie do momentu, w którym przekroczysz przydział. Agregacje wykonywane po stronie bazy są tu tańsze pod każdym względem.
Neon a alternatywy
| Cecha | Neon | Supabase | PlanetScale | Turso |
|---|---|---|---|---|
| Silnik | PostgreSQL | PostgreSQL | MySQL | zgodny z SQLite |
| Rozgałęzianie bazy | tak, mocna strona | ograniczone | tak | tak |
| Skalowanie do zera | tak | częściowo | nie | nie dotyczy |
| Warstwa aplikacyjna | brak, sama baza | autoryzacja, pliki, funkcje | brak | brak |
| Rozliczenie | za użycie | plany stałe i użycie | za użycie | za wiersze |
| Właściciel | Databricks | niezależny | niezależny | niezależny |
Wybór zależy od tego, czego szukasz. Jeśli potrzebujesz samej bazy i zależy Ci na rozgałęzianiu oraz na braku kosztu przy zerowym ruchu, Neon jest tu najmocniejszy. Jeśli chcesz dostać razem z bazą autoryzację, magazyn plików i funkcje, wybierz Supabase i oszczędź sobie składania tego z klocków. Jeśli obciążenie jest zdominowane przez odczyt i zależy Ci na opóźnieniu, warto spojrzeć na rozwiązania z repliką w aplikacji.
Aktualne stawki opisuje strona cennika Neon, a kod silnika znajdziesz w repozytorium projektu.