# Fit-check técnico — a arquitetura aguenta o workload?

> Companion ao [`migracao-infra-vds.md`](./migracao-infra-vds.md). Pergunta única:
> **"Isso que estamos sugerindo realmente suporta o que a Legacy Tech faz hoje?"**
>
> Resposta curta: **sim, mas com 2 ressalvas de tuning** documentadas no §11. Análise abaixo é numérica, baseada nas tabelas reais do `docs/codebase/INTEGRATIONS.md` + `CONCERNS.md` + perfis de uso típicos de cada engine.
>
> Atualização 2026-05-06.

---

## 1. Princípio arquitetural — OLTP vs OLAP

A Legacy Tech tem **duas naturezas de dado** muito distintas, e a separação ClickHouse + Postgres não é arbitrária — é dictada pelos padrões de acesso:

| Característica | OLTP (Postgres) | OLAP (ClickHouse) |
|---|---|---|
| Padrão de query | `WHERE id = X` (1 row) ou `JOIN` 2-5 tabelas | `SUM/AVG/GROUP BY` em milhões–bilhões de rows |
| Cardinalidade típica de SELECT | 1–100 rows | 10⁶–10⁹ rows agregados a 10² rows |
| Escrita | Transacional (1 row/operation), ACID rigoroso, FK | Append-only em batch (insert 1k–100k rows/op) |
| Update/Delete | Frequente, in-place | Raríssimo (`ALTER TABLE ... UPDATE` é caro) |
| Estrutura de armazenamento | **Row-based** (linha completa contígua em disco) | **Column-based** (cada coluna em arquivo separado, comprimida) |
| Compressão típica | 2–3× | 5–15× |
| Latência alvo | sub-10ms p50 | sub-1s p95 (queries complexas) |

**Erro arquitetural clássico:** rodar dashboard analítico sobre Postgres com índices criativos. Funciona para 1M de rows, sufoca em 100M, morre em 1B. **A Legacy já passou desse ponto** (1–3TB em CH) — mover analytics pra Postgres seria regressão. Manter a divisão é o caminho certo.

---

## 2. ClickHouse — o que vive lá e por quê

### 2.1. Tabelas em produção (`arbitragem`)

Da [`INTEGRATIONS.md`](../docs/codebase/INTEGRATIONS.md) + [`ARCHITECTURE.md`](../docs/codebase/ARCHITECTURE.md):

| Tabela | Origem | Escrita | Padrão de query | Justifica CH? |
|---|---|---|---|---|
| `fb_reports_raw` | Meta Ads connector (hourly) | INSERT batch ~1k–10k rows/h | `SUM(spend)` GROUP BY date/site/campaign | ✅ análise temporal massiva |
| `gam_raw` | GAM direct + ActiveView + AdSeleto | INSERT batch hourly | `SUM(impressions, revenue)` GROUP BY ad_unit/date | ✅ |
| `gam_hourly` | GAM hourly (delay tracking) | INSERT hourly | `MAX(date) WHERE site_id=X` (delay badge) | ✅ |
| `google_ads` | Google Ads connector | INSERT diário | `SUM` por campaign/date | ✅ |
| `gam_utm_campaign` | gam-utm-campaign connector | INSERT batch | atribuição UTM × revenue | ✅ |
| `ad_telemetry_raw` (Shadow-MCM) | Telemetria client-side | INSERT alta frequência | `15m_agg` materialized view | ✅ **especialmente bom em CH** |
| `ad_telemetry_15m_agg` | MV de `ad_telemetry_raw` | refresh contínuo via MV | dashboards real-time | ✅ |
| `view_master_radar` (view) | JOIN cross-tabelas | n/a — view | KPI dashboard 41 colunas | ✅ |
| `view_master_performance` | JOIN cross-tabelas | n/a — view | KPI dashboard 38 colunas | ✅ |

> ⚠️ **3 tabelas residuais de connectors mortos** (`gdelt_trends_signals`, `serpapi_trends`, `rss_trends_signals`) — **não migrar**. Drop antes do cutover.

### 2.2. Por que columnar é decisivo aqui

Caso concreto: `SELECT SUM(spend) FROM fb_reports_raw WHERE company_id=2 AND date >= '2026-01-01' GROUP BY site_id`.

- **Row-based (Postgres):** lê todas as colunas de cada row, mesmo precisando só de `company_id`, `date`, `site_id`, `spend` (4 colunas de ~30+ no schema). Disk I/O proporcional ao tamanho da row inteira.
- **Column-based (CH):** lê **apenas as 4 colunas necessárias**, cada uma comprimida 5–15× via codec (LZ4/ZSTD/Delta+ZSTD). Disk I/O = ~4 colunas × compressão. **Resultado: 30–100× menos I/O**.

Pro workload de monetization da Legacy (queries que somam impressions/spend por janelas de tempo), columnar é **uma ordem de grandeza mais rápido + barato em RAM/disco**.

### 2.3. ORDER BY (primary key) já está certo

Da [`CONCERNS.md`](../docs/codebase/CONCERNS.md) #432:

> `ORDER BY (company_id, site_id, date, ...)` — esse é o padrão recomendado pra workload multi-tenant da Legacy. Filtros mais comuns (`WHERE company_id=X AND site_id=Y AND date BETWEEN ...`) **batem direto na primary key** + skip indexes do CH.

A migração **não muda PRIMARY KEY** — `clickhouse-backup` preserva DDL, então layout fica idêntico ao Cloud.

### 2.4. Sizing pra 1–3TB raw

**Compressão real esperada:**

| Tipo de coluna | Codec recomendado | Razão típica |
|---|---|---|
| `Int32`/`Int64` (IDs, counts) | ZSTD(3) ou Delta+ZSTD | 5–10× |
| `Float64` (revenue, spend) | Gorilla+ZSTD | 4–8× |
| `String` baixa cardinalidade (data_source, currency) | LowCardinality(String) | 10–20× |
| `String` alta cardinalidade (URL, campaign_name) | ZSTD(3) | 3–6× |
| `DateTime` | DoubleDelta+ZSTD | 8–15× |

**Estimativa concreta:**
- 1–3TB raw → **200–500GB comprimido** em disco
- Margem de crescimento 24 meses (~2× volume): **400GB–1TB**
- VDS XXL traz 2× 1.6TB NVMe = 3.2TB → **folga de 2–3× sobre o pico projetado**

**RAM:**
- Regra ClickHouse: ~1GB RAM por 100GB de dado quente comprimido para queries confortáveis
- 500GB comprimido → 5GB mínimo. Mas queries com `GROUP BY` de alta cardinalidade + `ORDER BY` usam mais.
- **Reservar 32–48GB pra CH** (≈ 25–35% da VDS XXL) com `max_memory_usage_for_user = 16GB` por usuário pra evitar query rogue derrubar tudo.

**CPU:**
- CH paraleliza queries em todos os cores disponíveis (`max_threads`)
- 16 vCPU dedicados sustentam 2–4 dashboards concorrentes pesados confortavelmente
- Picos de merge background (parts → larger parts) usam 2–4 cores em surto

**Disk IOPS — única preocupação real:**
- ClickHouse merges são I/O-bound. NVMe Contabo é compartilhado no host físico ("noisy neighbor")
- **Mitigação obrigatória:** rodar `fio --rw=randread --bs=8k --iodepth=32 --numjobs=4 --runtime=60` no setup. Esperado: ≥ 50k IOPS sustained. Se < 30k, abrir ticket Contabo ou trocar de tier.

### 2.5. Por que não BigQuery / Snowflake / DuckDB

| Alternativa | Por que não |
|---|---|
| **BigQuery** | Já desabilitado (memória `tech_stack` + `CONCERNS.md`) — anomalia Fev/26 R$ 6.860 motivou migração pra CH Cloud |
| **Snowflake** | Cloud-only, custo proibitivo para volume da Legacy |
| **DuckDB** | Single-process, sem concorrência real, sem replicação. OK pra ad-hoc local, **ruim pra dashboard multi-user produção** |
| **TimescaleDB** | Postgres extension. Bom pra time-series puro, mas Legacy tem queries com JOIN cross-tabelas pesadas — CH ganha |

ClickHouse self-hosted é o ponto certo: **mesmo engine que Cloud** (zero refactor), open source, controlamos sizing.

---

## 3. Postgres — o que vive lá e por quê

### 3.1. Tabelas em produção (`gpc_ads`)

Da [`STACK.md`](../docs/codebase/STACK.md) + [`schema.prisma`](../../plenus-builder/prisma/schema.prisma) (50+ models):

| Domínio | Tabelas | Tipo de query |
|---|---|---|
| **Identity & multi-tenancy** | `users`, `user_companies`, `companies`, `sites` | `WHERE id=X` + JOIN — **OLTP clássico** |
| **Auth** | `api_keys`, `api_credentials`, `theme_licenses` | lookup por chave, valida + retorna |
| **Pipeline Shenlong** | `shenlong_pipeline_configs`, `shenlong_pipeline_runs`, `shenlong_keywords` | run-by-run, pequeno volume, joins |
| **Posts/Conteúdo** | `posts`, `authors`, `categories` | CRUD editorial — perfeito Postgres |
| **Operacional** | `connector_sync_configs`, `build_history`, `ai_costs` | logs operacionais; Postgres serve OK |
| **Cron locks** | `redis-lock` (Redis, não PG) | distribuído |

### 3.2. Por que row-based é certo aqui

- **Transacional:** criar conta de usuário = INSERT em `users` + INSERT em `user_companies`. Precisa de ACID. CH não dá.
- **FK + integridade referencial:** `posts.author_id → authors.id`. CH não tem FK.
- **Update frequente:** `UPDATE sites SET last_synced_at = NOW() WHERE id=X`. CH abomina updates.
- **Joins de 5–10 tabelas em 1 endpoint:** pequenas (centenas de rows), Postgres tem query planner maduro pra otimizar.

### 3.3. Sizing

**Volume:** 50+ tabelas, ~5–20GB total estimado (estimativa baseada no fato de que Cloud SQL prod nunca apareceu como gargalo). Mesmo com 5× crescimento em 2 anos = 25–100GB. **Trivial.**

**RAM:** Postgres feliz com `shared_buffers = 25%` da RAM dedicada + `effective_cache_size = 75%`. Reservar **16GB pra Postgres principal** (`gpc_ads`).
- `shared_buffers = 4GB`
- `work_mem = 64MB × max_connections`
- `effective_cache_size = 12GB`
- `maintenance_work_mem = 1GB`
- Resto fica disponível pro page cache do kernel.

Postgres dedicados (Infisical + GlitchTip): 2GB cada = 4GB.

**CPU:** 2–4 vCPU sustentam carga atual com folga.

**Disk:** 30–50GB volume + WAL retention 7d (~10GB) + pgBackRest local (~30GB). Reservar 100GB no `/var/lib/docker/volumes/postgres-*`.

### 3.4. Conexões — pgBouncer obrigatório

Hoje Prisma usa pool default 10 conexões por instância. Em Swarm com 2 réplicas do Dash + 14 connectors workers + pipeline Shenlong = **dezenas de conexões simultâneas**. Postgres não escala bem em conexões diretas (cada conexão = ~10MB RAM + processo dedicado).

**Solução:** **pgBouncer em transaction pooling mode** entre apps e Postgres. 100 client connections → 20 server connections. **Sem pgBouncer, sob load = travamento certo.** Já está no plano (§3.2 do `migracao-infra-vds.md`).

---

## 4. Redis — fit pra Bull queue + locks + cache

### 4.1. Workloads atuais

- **Bull queue** (post-worker.ts) — fila pra geração assíncrona de posts. Volume: ~348 posts/mês (Shenlong) → < 1 job/min de pico.
- **Distributed locks** (`redis-lock.ts`) — coordenação cron jobs.
- **Session cache** — TTLs curtos.
- **Query result cache** — opcional, mostly em algumas rotas.

### 4.2. Sizing

- **RAM:** workload atual <500MB residente. Reservar **2GB** pra folga.
- **CPU:** Redis é single-threaded por design. 1 core dedicado é mais que suficiente.
- **Persistência:** AOF every-second + RDB hourly. ~1GB de dump RDB max. Backup via Restic dos volumes.

**Sem surpresa.** Self-hosted Redis 7 substitui Upstash 1:1, com latência **menor** (loopback vs HTTP cross-region).

---

## 5. MinIO — fit vs GCS

### 5.1. Workloads

- `gpc-theme-packages`: ZIPs de tema WP (writes raros, reads em deploy de site)
- `shenlong-artifacts`: artifacts de pipeline IA (writes batch, reads por job)
- `xgboost-models`: modelos frozen — **deletar antes de migrar**
- **Novo:** target de `clickhouse-backup` + Restic

### 5.2. Compatibilidade S3 API

MinIO implementa S3 API completa. Código Python existente (`google-cloud-storage`) precisa ser trocado por `boto3`/`minio` Python SDK — **mudança de ~30 linhas no `gpc_common`** (~2h dev). Workflow GHA + connectors usam env var `STORAGE_PROVIDER` pra trocar.

### 5.3. Performance

- MinIO em NVMe local sustenta **400+ MB/s read/write** com erasure coding básico.
- Vs GCS multi-region: **mais rápido pra workloads internos** (loopback) + **mais lento pra users de outros DCs** (mas Cloudflare cacheia tudo público).

### 5.4. Capacity planning

| Bucket | Tamanho atual estimado | 24m projetado |
|---|---:|---:|
| theme-packages | <10GB | <50GB |
| shenlong-artifacts | <50GB | <200GB |
| ch-backup | n/a | 200–600GB (incremental + 1 full retido) |
| pg-backup (pgBackRest) | n/a | 30–100GB |
| restic-local | n/a | 200–500GB (snapshots 14d) |
| **Total bucket data** | <60GB | **~800GB–1.5TB** |

Cabe folgado em 1 dos 2× 1.6TB NVMe. Recomendação: **dedicar o 2º NVMe inteiro pra MinIO + backups**, deixando o 1º pra ClickHouse + Postgres + apps.

---

## 6. Cloud Tasks → Celery — equivalência semântica?

### 6.1. O que Cloud Tasks oferece hoje

Da [`INTEGRATIONS.md`](../docs/codebase/INTEGRATIONS.md):

| Queue | Rate | Concurrency | Workload |
|---|---|---|---|
| `queue-meta-ads` | 2/s | 20 | Sync FB Ads |
| `queue-activeview` | 1/s | 10 | Sync ActiveView |
| `queue-gam-soap` | 2/s | 10 | Sync GAM SOAP |
| `queue-gam-hourly` | 0.5/s | 3 | Sync GAM hourly |

Garantias: durable delivery, retry exponential backoff, dispatch via OIDC pra Cloud Run.

### 6.2. O que Celery oferece

```python
@app.task(
    rate_limit='2/s',          # ← matches queue-meta-ads
    autoretry_for=(RequestException,),
    retry_backoff=True,
    retry_backoff_max=600,
    retry_jitter=True
)
def sync_meta_ads(site_id: int) -> None: ...
```

E em `celery_app.conf`:
```python
task_routes = {
    'connectors.meta_ads.*': {'queue': 'meta-ads'},
    'connectors.activeview.*': {'queue': 'activeview'},
}
worker_concurrency = 20  # ← matches Cloud Tasks max concurrency
```

Subir 1 worker Celery por queue com `--concurrency=20`/`=10`/`=10`/`=3` espelha **exatamente** o limite atual.

### 6.3. Diferenças sutis (importantes)

| Aspecto | Cloud Tasks | Celery + Redis |
|---|---|---|
| Durabilidade | At-least-once garantida pelo GCP | Depende de Redis persistence (AOF every-second) — **arrisca perder ≤1s de tasks em crash** |
| Dispatch | HTTP OIDC pull | broker Redis, worker pull |
| Retry policy | Configurada no GCP | Decorator do Celery |
| Visibility | Cloud Console UI | Flower (UI Celery) self-hosted |
| Rate limiting | Server-side enforced | Worker-side enforced (token bucket per worker — **diferente!**) |
| Scheduling | Cloud Scheduler dispara | Celery Beat ou Ofelia |

### 6.4. Pegadinha do rate limit

**Atenção:** Celery `rate_limit='2/s'` é **por worker**, não global. Se subir 2 workers concurrent → na prática faz 4/s.

**Solução:** rodar **1 worker por queue** (sem replicas) com concurrency interno alto. Ou usar **token bucket no Redis** pra rate limit global (lib `celery-rate-limit`).

Decisão recomendada: **single worker per queue + alta concurrency** (2/s × 20 conc = capaz de fazer 40 ops/min, idêntico ao GCP). Adicionar Flower pra observabilidade.

### 6.5. Backup B se Celery não bater

Se experiência mostrar que rate limit per-worker quebra paridade, plano B = **Hatchet** (workflow engine OSS Postgres-based, durable execution, rate limit global). Migração trivial.

---

## 7. Cloud Scheduler → Ofelia — equivalência

Cloud Scheduler hoje dispara 5 cron jobs:
- `gpc-sync` — sync KPIs (cada 5min)
- `gpc-triggers` — execute rules (cada 10min)
- `shenlong-scheduler` — agendar conteúdo (hourly)
- `shenlong-recovery` — retry stuck (cada 30min)
- `post-scheduled` — flush queue (cada 5min)

**Ofelia substitui via Docker labels:**
```yaml
labels:
  ofelia.enabled: "true"
  ofelia.job-exec.gpc-sync.schedule: "*/5 * * * *"
  ofelia.job-exec.gpc-sync.container: "plenus-builder"
  ofelia.job-exec.gpc-sync.command: "node scripts/cron/gpc-sync.js"
```

**Equivalência funcional:** ✅ idêntica.
**Diferença:** Cloud Scheduler tem retry automático em caso de erro HTTP non-2xx; Ofelia roda comando 1×, sem retry nativo. **Mitigação:** lógica de retry dentro do job (já existe em `lib/redis-lock.ts` com retry).

---

## 8. Vercel ISR/SSR → Coolify + Redis

### 8.1. O que Vercel faz que importa

- **SSR streaming** (Next 15 App Router) — funciona em qualquer runtime Node 20
- **ISR** (Incremental Static Regeneration) — `revalidate: N` segundos
- **On-demand revalidation** — `revalidatePath()` / `revalidateTag()`
- **Edge Functions** — middleware roda na borda
- **Image Optimization** — `next/image`

### 8.2. Cabe em Swarm?

| Feature Vercel | Funciona em Coolify+Swarm? | Como |
|---|---|---|
| SSR streaming | ✅ nativo | Next.js standalone build |
| ISR (file-based cache padrão) | ⚠️ **não em multi-replica** | Cache padrão é local — réplicas vão divergir |
| ISR com cache compartilhado | ✅ via OpenNext + Redis | `@neshca/cache-handler` ou OpenNext custom cache |
| `revalidatePath`/`revalidateTag` | ✅ via Redis pub/sub | Notifica todas as réplicas pra invalidar |
| Edge middleware | ✅ no Coolify roda como Node middleware | Sem edge real, mas funciona |
| `next/image` | ✅ | Sharp em Node, ou Cloudflare Polish na frente |
| Preview deploys (URL única por PR) | ⚠️ trabalhoso | Coolify tem "branch deploys" mas precisa configurar |

### 8.3. Cache handler obrigatório

```typescript
// next.config.ts
export default {
  output: 'standalone',
  cacheHandler: require.resolve('@neshca/cache-handler'),
  cacheMaxMemorySize: 0  // desliga in-memory cache, força Redis
}
```

E `cache-handler.js`:
```javascript
const { CacheHandler } = require('@neshca/cache-handler')
const Redis = require('ioredis')

CacheHandler.onCreation(async () => ({
  handlers: [{
    name: 'redis-handler',
    handler: require('@neshca/cache-handler/redis-stack')({ client: new Redis(process.env.REDIS_URL) })
  }]
}))
```

**Custo de implementação:** ~1 dia de Berim. Está no escopo da Fase 5.

### 8.4. Imagens — substituir Vercel Image Optimization

Opções:
1. **Cloudflare Polish** (Pro plan $20/mês) — automático na borda, zero código
2. **`sharp` no servidor** — mais carga no app, mas free
3. **`next-image-export-optimizer`** — pré-compila no build, melhor pra estático
4. **Plaiceholder + sharp** — manual mas controle total

Recomendação: **CF Polish** se já contratarmos Pro plan, senão **sharp built-in** (Next 15 já vem).

---

## 9. Backup throughput — 1–3TB CH em B2 semanal cabe na janela?

### 9.1. Cálculo de upload

Contabo VDS XXL: porta 1Gbps = **125 MB/s sustained** (descontando overhead).

| Volume | Tempo upload single full | Custo B2 (storage) | Custo B2 (egress restore) |
|---|---|---:|---:|
| 500GB | ~70 min | $3/mês | $0 (CF Bandwidth Alliance) |
| 1TB | ~2.3h | $6/mês | $0 |
| 2TB | ~4.5h | $12/mês | $0 |
| 3TB | ~6.8h | $18/mês | $0 |

**Janela sábado 02:00–10:00** absorve 3TB tranquilo.

### 9.2. Estratégia incremental + full

`clickhouse-backup` suporta full + incremental:
- **Domingo 02:00:** full (uma vez/semana) = 200–600GB comprimido
- **Seg–Sáb 03:00:** incremental = 5–30GB cada (delta de inserts diários)
- **Mensal:** full retido 12m

**Tráfego semanal real upload pra B2:** ~250–700GB (1 full + 6 incrementais). **30–60min/dia em pico**, sem impactar workload.

### 9.3. Restore RTO real

| Cenário | Volume | Download (B2 → VDS) | Restore CH | Total |
|---|---|---|---|---|
| Restaurar 1 tabela isolada (~50GB) | 50GB | 7min | 5min | **~12min** |
| Restaurar full 1TB | 1TB | 2.3h | 30–45min | **~3h** |
| DR completa 3TB | 3TB | 6.8h | 1h | **~8h** |

RTO declarado no plano (`migracao-infra-vds.md` §6.4) = **2–4h pra ClickHouse perda total** ⇒ válido pra DBs até ~1.5TB. Acima disso, refinar pra 4–8h.

---

## 10. Latência — DC EU vs hoje (SA-East-1 + US-Central-1)

### 10.1. RTT real (medições típicas 2026)

| Origem → Destino | RTT médio |
|---|---|
| **Nuremberg DE** → São Paulo BR | 200–220ms |
| **Nuremberg DE** → Meta Graph API (US) | 95–110ms |
| **Nuremberg DE** → Google Ads/GAM API (US) | 90–105ms |
| **Nuremberg DE** → Cloudflare Edge (qualquer PoP) | 1–10ms |
| Hoje: us-central1 → São Paulo | 130–150ms |
| Hoje: us-central1 → Meta API | 30–50ms |

### 10.2. Impacto por caminho

| Caminho de request | Hoje | Pós-migração | Impacto |
|---|---|---|---|
| User BR → CDN Cloudflare | 5–20ms | 5–20ms | **= sem mudança** (Cloudflare cacheia 80%+ do estático) |
| User BR → SSR dynamic | 130–150ms (Vercel sa-east-1) | 200–220ms (DE) | ⚠️ **+70–90ms** |
| Connector → ClickHouse | 30ms (us → us-central) | 0.1ms (loopback) | ✅ **−30ms** |
| Connector → Postgres | 50ms (us → SA Cloud SQL) | 0.1ms (loopback) | ✅ **−50ms** |
| Connector → Meta API | 30–50ms | 95–110ms | ⚠️ **+50–80ms** |

### 10.3. Net effect

- **Connectors ficam mais rápidos** (loopback DB) — ganho líquido em pipelines internos
- **API outbound (Meta/GAM) fica mais lento** — soma 50–80ms por request × milhares de requests/h. Em sync hourly de Meta Ads, traduz pra ~5–10min a mais por ciclo. **Aceitável** — sync já tem janela de horas pra completar.
- **User BR no SSR fica mais lento ~80ms** — perceptível mas **só em rotas dynamic**. Cloudflare cache cobre o resto. Aceito como trade-off do plano.

### 10.4. Mitigação opcional

Se latência SSR pra users BR virar issue:
- Cloudflare Workers pra rotas dynamic na borda (cache curto + revalidate em background)
- OU spawnar VDS secundária em SA-East (Hetzner Brazil ou similar) só pra SSR, com Postgres replica via streaming. **Defere pra fase 2** — começar com 1 VDS DE.

---

## 11. Sizing somado — cabe na VDS XXL (192–256GB / 16–24 vCPU)?

### 11.1. Worst-case RAM (uso simultâneo)

| Componente | RAM reservada | Notas |
|---|---:|---|
| ClickHouse | 48 GB | `max_memory_usage_for_user=16GB`, server total 48GB folgado |
| Postgres principal (`gpc_ads`) | 16 GB | shared_buffers 4GB + cache 12GB |
| Postgres Infisical | 2 GB | dedicado, pequeno |
| Postgres GlitchTip | 2 GB | dedicado |
| Redis | 2 GB | folga sobre <500MB real |
| MinIO | 2 GB | escala com carga |
| LGTM stack (Prom+Loki+Tempo+Grafana+AM+Alloy+exporters) | 8 GB | estimativa pesada |
| GlitchTip web+worker | 1 GB | |
| Uptime Kuma | 256 MB | |
| 14 connectors Celery (média 512MB cada) | 7 GB | I/O bound, não CPU |
| Celery Beat + Flower | 512 MB | |
| Coolify | 1 GB | |
| 2× GHA runners (idle) | 2 GB | até 8GB em build pesado |
| Harbor | 3 GB | se ativado |
| Traefik + CF Companion + cloudflared | 512 MB | |
| Plenus-builder Next.js (2 réplicas) | 4 GB | 2GB cada |
| Sistema Linux + Docker daemon | 4 GB | |
| **Total nominal** | **~104 GB** | |
| Buffer + page cache | **24 GB** | sobra |

**Veredito:** ✅ cabe na XXL **com folga de ~46%** (sobram ~88GB livres pra page cache + burst).

### 11.2. Pico simultâneo (build GHA + dashboard pesado + sync connectors)

- Build GHA pesado pode subir +6–8GB temporariamente
- Dashboard query CH custosa pode subir +8–12GB
- 14 connectors em sync simultâneo ≈ +5GB acima do baseline

**Worst case temporário: ~125–135GB.** Pode bater swap (16GB planejado).

**Mitigações:**
1. Schedule builds GHA fora de horário comercial (cron do GHA workflow ou janela)
2. Limitar concurrent builds em 1 (queue runners, label `legacy`)
3. CH `max_memory_usage_for_user=16GB` (já recomendado §2.4)
4. Celery worker concurrency hard limit (já recomendado §6.4)

### 11.3. Recomendação alternativa — VDS XXL (256GB)

Se preço viável (~+€60–80/mês), **subir pra VDS XXL com 192–256GB RAM** elimina toda essa preocupação. **Recomendação: cotar XXL como upgrade na fase 0.** Custo extra ~R$ 400–500/mês vale a pena pra eliminar tuning agressivo.

### 11.4. CPU 16 vCPU EPYC dedicados

| Componente | Uso típico | Pico |
|---|---|---|
| ClickHouse | 1–2 cores | 8–16 em query pesada |
| Postgres | 0.5 core | 2–4 em vacuum/analyze |
| Connectors (14 workers) | 1–2 cores | 4 em sync simultâneo |
| Next.js (2 réplicas) | 0.5 core | 2 em SSR pico |
| GHA build | 0 idle | 4–8 em build pesado |
| LGTM | 0.5 core | 1 |
| MinIO | 0.5 | 2 em backup |
| Sistema | 0.3 | 1 |
| **Baseline** | **~5 cores** | |
| **Pico simultâneo** | | **~25–30 cores requested** |

⚠️ **Pico simultâneo excede 16 vCPU.** Resultado: queue de CPU, latência sobe mas nada quebra. Mitigação:
1. Schedule build GHA fora de janela CH pesada
2. CH `max_threads = 8` (limita query parallel — leve degradação aceita)

### 11.5. Disk somado

| Volume | Tamanho 24m projetado |
|---:|---|
| ClickHouse data | 200–1000GB (comprimido) |
| Postgres `gpc_ads` | 30–100GB |
| Postgres Infisical/GlitchTip | 10GB |
| Redis dump | 1GB |
| MinIO buckets internos | 200–500GB |
| Logs Loki (chunks) | 50–100GB local + offload MinIO |
| Métricas Prom (30d) | 20–40GB |
| Tempo traces (31d) | 10–30GB |
| Backups locais Restic | 200–500GB |
| Imagens Docker Harbor | 50–200GB |
| Sistema + Docker | 100GB |
| **Total** | **870GB–2.6TB** |

VDS XXL = 3.2TB NVMe. **Folga ~600GB–2.3TB.** ✅ confortável.

---

## 12. SPOFs e degradação graceful

| Componente | SPOF? | Impacto se cai | Recovery |
|---|---|---|---|
| VDS inteira | ✅ SIM (single-node fase 1) | Tudo down | 4–8h provisão + restore B2 |
| Postgres principal | ✅ | Dash + connectors travam | pgBackRest restore < 30min |
| ClickHouse | ⚠️ data writes acumulam (Bull queue) | Dashboards 500 | clickhouse-backup restore 2–4h |
| Redis | Bull queue para | reinicia, perde tasks ≤1s | retry idempotent dos connectors compensa |
| MinIO | Backups param | downloads quebram | reinicia local |
| Cloudflare | external | tudo down externo | <1h SLA Cloudflare 99.99% |

### 12.1. Compensação na fase 1 (sem HA real)

- **RTO objetivo:** 4–8h pra DR completa, 30min pra falha de componente
- **RPO objetivo:** 5–15min pra Postgres (WAL streaming), 24h pra ClickHouse (último backup)
- **Mitigação ativa:**
  - Alertas Discord agressivos (componente down → notifica em 30s)
  - Healthchecks profundos no Uptime Kuma
  - Runbook DR documentado + impresso (Fase 7)

### 12.2. Quando justifica HA (fase 2 — futuro)

Se downtime > 1h/mês começar a doer:
- 2ª VDS Contabo em DC diferente como standby (Postgres streaming replica + CH replica via Keeper)
- Custo: +€60–90/mês
- Failover semi-manual via DNS Cloudflare (TTL 60s pré-config)

---

## 13. Conclusão — passa o fit-check?

**Sim, com 3 condições:**

1. ✅ **Arquitetura ClickHouse + Postgres + Redis + MinIO está correta** — espelha 1:1 o que GCP+CH Cloud fazem, com mesma engine. Zero mudança de schema necessária.
2. ✅ **VDS XXL (192–256GB) absorve sizing somado com folga de ~46%** — sem necessidade de tuning agressivo de RAM. Decisão final 2026-05-07.
3. ⚠️ **Latência API outbound (Meta/GAM) sobe ~50–80ms** — aceito (sync tem janela de horas). Latência SSR user BR sobe ~80ms — aceito (CF cobre estático). Reavaliar em 90d pós-cutover.

### 13.1. Ações concretas pré-fase 0 (essa semana)

| Ação | Owner | Saída |
|---|---|---|
| ~~Cotar tier final~~ — **decidido VDS XXL 2026-05-07** | Danniel | ✓ feito |
| Estimar volume real `arbitragem` em CH Cloud (`SELECT formatReadableSize(sum(bytes_on_disk)) FROM system.parts`) | Berim | Número exato pra dimensionamento |
| Estimar volume real `gpc_ads` Postgres (`SELECT pg_database_size('gpc_ads')`) | Berim | Idem |
| Benchmark fio na VDS provisionada (fase 1) | Danniel | IOPS sustained ≥ 50k |
| Validar pgBouncer config | Berim | Pool sizes corretos |
| Definir `max_memory_usage_for_user` no CH config | Berim | 16GB ou 24GB |

### 13.2. Pontos não-resolvidos a decidir antes da fase 1

- ~~Tier final~~ — **decidido VDS XXL 2026-05-07**
- **Hatchet vs Celery** — escolha vai depender se durable execution importa em algum connector específico
- **OpenNext + Cloudflare Workers** — adicional opcional sobre Coolify (poderia rodar SSR na borda + dynamic no servidor) — defere pra fase 2

### 13.3. Riscos residuais

| Risco | Probabilidade | Mitigação |
|---|---|---|
| ClickHouse cresce além de 3TB em 12m | Baixa | Particionamento por mês + TTL automático em tabelas raw |
| Postgres bate 100GB e queries lentas | Baixa | Prisma query analysis + índices ad-hoc; vacuum agressivo |
| Celery rate-limit per-worker quebra paridade Cloud Tasks | Média | Single worker per queue (já no plano) ou Hatchet como plano B |
| Cloudflare Workers free tier estoura | Baixa | $5/mês plano paid resolve |
| Backup full > janela weekend | Baixa | Incremental cobre — full mensal absorve picos |

---

## 14. Referência rápida — qual tech pra qual workload

```
┌─────────────────────────────────────────────────────────────────────┐
│ DASHBOARD/ANALYTICS    → ClickHouse  (columnar, 1-3TB, agregações)  │
│ APP STATE / AUTH       → Postgres    (row, ACID, FK, OLTP)          │
│ JOB QUEUE / LOCKS      → Redis       (in-memory, fast)              │
│ FILES / BACKUP / LOGS  → MinIO       (S3 API, NVMe local)           │
│ RATE-LIMITED TASKS     → Celery      (rate_limit, retry, scheduling)│
│ CRON                   → Ofelia      (Docker labels)                │
│ METRICS                → Prometheus  (TSDB, PromQL)                 │
│ LOGS                   → Loki        (chunks → MinIO)               │
│ TRACES                 → Tempo       (sampled OpenTelemetry)        │
│ SECRETS                → Infisical   (Postgres-backed, MIT)         │
│ ERRORS                 → GlitchTip   (Sentry-compatible)            │
│ UPTIME                 → Kuma        (HTTP/keyword/icmp)            │
│ CDN / EDGE / DNS / WAF → Cloudflare  (free tier + Tunnel)           │
│ IMAGE GEN              → Replicate   (API externa contratada)       │
│ LLM GATEWAY            → OpenRouter  (mantém)                       │
│ EMAIL TX               → Resend      (mantém)                       │
└─────────────────────────────────────────────────────────────────────┘
```

---

*Fit-check executado 2026-05-06 · atualizado 2026-05-07 com decisão fechada VDS XXL. Documento companion ao [`migracao-infra-vds.md`](./migracao-infra-vds.md).*
