Escalando aplicações SaaS com PostgreSQL
Estratégias de arquitetura para crescer com confiança e previsibilidade.
O desafio de escalar um SaaS
Escalar um produto SaaS não é só adicionar mais servidores. A camada de dados é geralmente o primeiro gargalo quando você cresce. Vamos explorar estratégias testadas em produção para escalar PostgreSQL de centenas para milhões de usuários.
1. Estágios de crescimento típicos
Fase 1: MVP (0-1k usuários)
Arquitetura:
- 1 servidor PostgreSQL (2-4 vCPU, 8-16GB RAM)
- Aplicação e banco no mesmo servidor
- Sem cache, sem read replicas
Características:
✅ Simples de manter
✅ Custo baixo ($50-100/mês)
⚠️ Single point of failure
⚠️ Performance limitada
Fase 2: Product-Market Fit (1k-10k usuários)
Arquitetura:
- Aplicação separada do banco
- PostgreSQL dedicado (4-8 vCPU, 16-32GB RAM)
- Redis para cache e sessions
- Backups automáticos
Otimizações necessárias:
✅ Índices otimizados
✅ Connection pooling (PgBouncer)
✅ Cache de queries frequentes
✅ Monitoramento ativo
Fase 3: Escala (10k-100k usuários)
Arquitetura:
- Primary + 1-2 Read Replicas
- PgBouncer para pooling
- Redis cluster (cache distribuído)
- CDN para assets
- Background jobs (Sidekiq/Celery)
Características:
✅ Alta disponibilidade
✅ Leitura escalável horizontalmente
⚠️ Escritas ainda no primary
⚠️ Complexidade aumentada
Fase 4: Alta escala (100k+ usuários)
Arquitetura:
- Sharding ou particionamento
- Múltiplas read replicas (5-10+)
- Cache multi-camada (L1/L2)
- CQRS (separação read/write)
- Event sourcing opcional
Estratégias:
✅ Particionamento por tenant
✅ Separação de microserviços
✅ Data warehousing separado
✅ Observabilidade completa
2. Connection Pooling - A fundação
O problema de conexões
-- PostgreSQL cria 1 processo por conexão
-- Cada processo consome ~10MB RAM
# Exemplo: 1000 conexões simultâneas
1000 conexões × 10MB = 10GB RAM só para conexões! ❌
# Solução: PgBouncer
100 conexões da app → PgBouncer → 20 conexões reais ao Postgres ✅
Economia: 9.8GB RAM + performance melhor
Configurando PgBouncer
# pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=production
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# Pool configuration
pool_mode = transaction # Mais eficiente
max_client_conn = 1000 # Conexões da aplicação
default_pool_size = 25 # Conexões reais ao Postgres
reserve_pool_size = 5
reserve_pool_timeout = 3
# Timeout settings
server_idle_timeout = 600
query_timeout = 30
Connection string na aplicação
// Antes: Direto ao PostgreSQL
// postgres://user:pass@postgres:5432/mydb
// Depois: Via PgBouncer
const pool = new Pool({
host: 'pgbouncer',
port: 6432, // Porta do PgBouncer
database: 'mydb',
user: 'app_user',
password: 'senha',
max: 20, // Pool da aplicação
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
3. Read Replicas - Escale leituras
Quando usar read replicas
- ✅ Leitura > 70% das operações (típico em SaaS)
- ✅ Queries de analytics/relatórios pesadas
- ✅ Dashboard público com muitos acessos
- ✅ APIs de consulta sem escrita
Roteamento inteligente
// Node.js - Separação read/write
const writePool = new Pool({ host: 'primary.postgres', port: 5432 });
const readPool = new Pool({ host: 'replica.postgres', port: 5432 });
// Função auxiliar
function getPool(operation = 'read') {
return operation === 'write' ? writePool : readPool;
}
// Uso
async function getUser(id) {
const pool = getPool('read');
return pool.query('SELECT * FROM users WHERE id = $1', [id]);
}
async function updateUser(id, data) {
const pool = getPool('write');
return pool.query('UPDATE users SET name = $1 WHERE id = $2', [data.name, id]);
}
// Rails - config/database.yml
production:
primary:
host: primary.postgres
replica:
host: replica.postgres
replica: true
# Uso automático
User.find(123) # Vai para replica
User.update(123, name: 'João') # Vai para primary
Lidando com replication lag
// Problema: "Read your writes"
// User acabou de criar post, mas replica ainda não tem
// Solução 1: Force primary após write
async function createPost(userId, data) {
const pool = getPool('write');
const result = await pool.query(
'INSERT INTO posts (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, data.title]
);
// Próximas leituras vão para primary (5 segundos)
setReadPreference(userId, 'primary', 5000);
return result.rows[0];
}
// Solução 2: Sticky sessions
// Requests do mesmo user sempre na mesma replica
// Solução 3: Mostre loading state
// "Seu post está sendo processado..."
4. Particionamento - Dividir para conquistar
Particionamento por data (Time-series)
-- Exemplo: Tabela de logs
CREATE TABLE logs (
id BIGSERIAL,
user_id INT,
event_type VARCHAR(50),
created_at TIMESTAMP NOT NULL,
data JSONB
) PARTITION BY RANGE (created_at);
-- Crie partições mensais
CREATE TABLE logs_2026_01 PARTITION OF logs
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE logs_2026_02 PARTITION OF logs
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- Índices em cada partição
CREATE INDEX idx_logs_2026_01_user ON logs_2026_01(user_id);
CREATE INDEX idx_logs_2026_02_user ON logs_2026_02(user_id);
-- Queries automáticas na partição correta
SELECT * FROM logs
WHERE created_at >= '2026-02-10'
AND created_at < '2026-02-15';
-- PostgreSQL busca só em logs_2026_02 ✅
Particionamento por tenant (Multi-tenancy)
-- Estratégia 1: Particionamento por tenant_id
CREATE TABLE orders (
id BIGSERIAL,
tenant_id INT NOT NULL,
customer_id INT,
amount DECIMAL,
created_at TIMESTAMP
) PARTITION BY HASH (tenant_id);
-- Crie 16 partições (ajuste conforme necessário)
CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE orders_p1 PARTITION OF orders FOR VALUES WITH (MODULUS 16, REMAINDER 1);
-- ... até p15
-- Queries incluem tenant_id (obrigatório!)
SELECT * FROM orders WHERE tenant_id = 123 AND status = 'pending';
-- Busca só na partição correta
Estratégia 2: Schema por tenant
-- Cada tenant tem seu próprio schema
CREATE SCHEMA tenant_123;
CREATE TABLE tenant_123.orders (...);
CREATE TABLE tenant_123.customers (...);
CREATE SCHEMA tenant_456;
CREATE TABLE tenant_456.orders (...);
-- Na aplicação
const tenantId = req.user.tenantId;
await pool.query(\`SET search_path TO tenant_\${tenantId}\`);
await pool.query('SELECT * FROM orders'); // Busca no schema correto
Estratégia 3: Banco por tenant (ultimate isolation)
-- Para tenants enterprise/grandes
tenant_123 → postgres://db1.cloudpg.com.br/tenant_123
tenant_456 → postgres://db2.cloudpg.com.br/tenant_456
// Roteamento dinâmico
const connectionString = getTenantDatabase(tenantId);
const pool = new Pool({ connectionString });
5. Caching estratégico
Camadas de cache
┌─────────────────────┐
│ Application │
│ (Memory cache) │ ← L1: 1-5ms (Node cache, dict em Python)
└─────────────────────┘
↓
┌─────────────────────┐
│ Redis/Memcached │ ← L2: 5-20ms
└─────────────────────┘
↓
┌─────────────────────┐
│ PostgreSQL │ ← L3: 20-100ms
└─────────────────────┘
Implementação de cache
// Node.js com Redis
const redis = require('redis').createClient();
async function getUser(id) {
// 1. Tenta cache L1 (memória)
if (memoryCache.has(\`user:\${id}\`)) {
return memoryCache.get(\`user:\${id}\`);
}
// 2. Tenta cache L2 (Redis)
const cached = await redis.get(\`user:\${id}\`);
if (cached) {
const user = JSON.parse(cached);
memoryCache.set(\`user:\${id}\`, user); // Popula L1
return user;
}
// 3. Busca no PostgreSQL
const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
const user = result.rows[0];
// 4. Popula caches
await redis.setex(\`user:\${id}\`, 3600, JSON.stringify(user)); // TTL 1h
memoryCache.set(\`user:\${id}\`, user);
return user;
}
// Invalidação ao atualizar
async function updateUser(id, data) {
await pool.query('UPDATE users SET name = $1 WHERE id = $2', [data.name, id]);
// Invalida cache
memoryCache.delete(\`user:\${id}\`);
await redis.del(\`user:\${id}\`);
}
6. Background jobs e filas
Separe operações assíncronas
// ❌ Ruim: Processamento síncrono
app.post('/upload-file', async (req, res) => {
const file = req.file;
await processFile(file); // 30 segundos ⏳
await generateThumbnails(file); // 15 segundos ⏳
await extractMetadata(file); // 10 segundos ⏳
await sendNotification(req.user); // 2 segundos ⏳
res.json({ success: true }); // Usuário esperou 57 segundos!
});
// ✅ Bom: Background jobs
app.post('/upload-file', async (req, res) {
const file = req.file;
// Salva referência no banco
await pool.query('INSERT INTO uploads (user_id, filename) VALUES ($1, $2)',
[req.user.id, file.name]);
// Enfileira jobs
await queue.add('process-file', { fileId: file.id });
await queue.add('generate-thumbnails', { fileId: file.id });
await queue.add('extract-metadata', { fileId: file.id });
res.json({ success: true, processing: true }); // Resposta instantânea ✅
});
// Worker processa em background
worker.process('process-file', async (job) => {
const { fileId } = job.data;
await processFile(fileId);
});
7. Monitoramento e alertas
KPIs essenciais para SaaS
| Métrica | Alerta Warning | Alerta Crítico |
|---|---|---|
| Query P95 latency | > 200ms | > 1000ms |
| Conexões ativas | > 70% max | > 90% max |
| Replication lag | > 5s | > 30s |
| Disco usado | > 70% | > 85% |
| Cache hit ratio | < 90% | < 80% |
| Deadlocks | > 5/hora | > 20/hora |
8. Escalando com CloudPG
CloudPG foi projetado para crescer com seu SaaS, desde MVP até enterprise:
FREE → BASIC (0-10k users)
- ✅ Upgrade vertical com 1 clique (CPU/RAM)
- ✅ PgBouncer incluído e otimizado
- ✅ Backups automáticos
- ✅ SSL obrigatório
PRO → GIGA (10k-100k users)
- ✅ Read replicas automáticas (1 clique)
- ✅ Connection pooling avançado
- ✅ Monitoramento real-time
- ✅ Alertas inteligentes (Slack/Email)
- ✅ PITR (Point-in-Time Recovery)
- ✅ Suporte prioritário 24/7
ENTERPRISE (100k+ users)
- ✅ Multi-region replication
- ✅ Sharding managed
- ✅ SLA 99.99%
- ✅ Dedicated account manager
- ✅ Custom infrastructure
- ✅ Compliance (SOC2, LGPD)
Conclusão
Escalar um SaaS com PostgreSQL é totalmente viável seguindo as estratégias certas:
- ✅ Connection pooling desde o dia 1
- ✅ Read replicas quando leitura > 70%
- ✅ Cache inteligente (L1 + L2)
- ✅ Particionamento para tabelas grandes
- ✅ Background jobs para operações pesadas
- ✅ Monitoramento ativo com alertas
Com CloudPG, você tem toda infraestrutura gerenciada para escalar sem dor de cabeça.
Escale com confiança
Comece grátis no CloudPG e escale de 0 a milhões de usuários com a mesma plataforma.