Performance
Benchmarks medidos com ab (HTTP real, TCP, keep-alive) e suite testing.B do Go.
Hardware: Intel Core i5-1235U · Go 1.26.2 · Linux · SERVER_WORKERS=4 (conforme .env).
HTTP real — ab (keep-alive, 100 conexões simultâneas)
| Cenário | Req/s | Latência média | Falhas |
|---|---|---|---|
GET /ping/ — rota estática |
127.474 | 0,784 ms | 0 |
GET /usuarios/42/ — path param |
126.454 | 0,791 ms | 0 |
GET /busca/?q=kyrux&page=3 — query string |
109.302 | 0,915 ms | 0 |
| 500 conexões simultâneas (pico) | 99.110 | 5,045 ms | 0 |
Zero falhas em 200.000 requisições totais.
Latência — percentis (100 conexões, 50k req)
| Percentil | Tempo |
|---|---|
| P50 | 0,89 ms |
| P90 | 1,34 ms |
| P95 | 1,72 ms |
| P99 | 2,44 ms |
| P100 (pior caso) | 6,36 ms |
Medição pontual num hardware específico — trate como ordem de grandeza, não como benchmark oficial reproduzível em qualquer máquina. Para números atuais no seu próprio ambiente:
go run main.go benchmark(roda as 3 camadas de teste do framework — microbenchmark, framework sem TCP e throughput real — e salva o resultado embenchmark/).
Rotas reais — banco de dados, admin e renderização HTML
A tabela acima usa respostas JSON estáticas, sem tocar banco — mede o custo
puro do router. O teste TestThroughputReal (core/router/benchmark/) mede
o resto: registra um model real no admin, semeia 200 linhas num Postgres de
verdade, cria uma sessão autenticada de verdade (login + cookie CSRF, não
um token forjado) e bate em cada rota descoberta via fw.Router.Routes().
| Rota | O que mede | Req/s | Falhas |
|---|---|---|---|
GET /ping/ |
Baseline: rota estática, sem banco | 39.250 | 0 |
GET / |
Welcome page padrão do framework, sem banco | 37.914 | 0 |
GET /admin/login/ |
Formulário de login (HTML), sem sessão | 24.957 | 0 |
GET /admin/<slug>/novo/ |
Formulário vazio de criação (HTML, autenticado) | 14.162 | 0 |
GET /admin/ |
Dashboard do admin (HTML, autenticado) | 17.878 | 0 |
GET /admin/<slug>/<pk>/ |
Uma linha por chave primária via ORM (HTML, autenticado) | 9.812 | 0 |
GET /admin/<slug>/ |
Lista paginada real via ORM — 200 linhas seedadas, SELECT + COUNT (HTML, autenticado) |
1.920 | 0 |
Zero falhas nas 7 rotas. A lista paginada é a mais cara (duas queries reais
por request, mais o custo de html/template iterando as linhas) — mesma
conclusão da página de comparativo com Django:
não é o banco que pesa (a query é rápida), é a renderização do template.
Medido via cliente Go (
net/http, no mesmo processo/GOMAXPROCSdo servidor — por isso os números são mais baixos que a tabelaabacima, que roda em processo separado),APP_ENV=production, Postgres real na mesma máquina. Reproduza comgo run main.go benchmark(ougo test ./core/router/benchmark/ -run TestThroughputReal -v -count=1) — o teste é autocontido: cria e popula sua própria tabela, cria seu próprio usuário staff, e limpa os dois ao final (defer DROP TABLE+defer DELETEdo usuário). Não faz parte deste site de docs (que roda sem banco,DB_ENABLED=false) — é do framework em si.
Benchmarks Go nativos — testing.B (GOMAXPROCS=4, sem TCP overhead)
| Cenário | ns/op | Req/s estimado | Allocs/op |
|---|---|---|---|
| Rota estática | 1.104 | ~906.000 | 15 |
| Path param | 1.313 | ~762.000 | 18 |
| Query string | 1.715 | ~583.000 | 22 |
| 1 middleware | 878 | ~1.139.000 | 12 |
| 3 middlewares | 908 | ~1.101.000 | 12 |
| Estático paralelo (4 cores) | 923 | ~4.334.000 | 15 |
| Path param paralelo (4 cores) | 697 | ~5.743.000 | 18 |
Notas
- +13% no throughput estático e path param em relação à versão anterior — ganho principal vem do
Content-Lengthnas respostas JSON, que elimina o overhead dechunked transfer encodingno HTTP/1.1. - Degradação sob 500 conexões: queda de ~14% esperada — com 4 workers, mais conexões competem pelo mesmo pool de goroutines. Aumentar
SERVER_WORKERSatenua isso. - Middlewares têm custo baixo: 3 middlewares encadeados custam menos de 30 ns a mais que 1 — o chain é compilado uma vez antes do request chegar.
- Query string cacheada por request:
ctx.Query(),ctx.QueryInt()etc. parseiam a URL uma única vez — chamadas subsequentes na mesma view reutilizam o resultado. - Gargalo esperado: rotas 404 em
developmentsão ~9× mais lentas (renderizam o template HTML de debug completo). Emproduction, a página estática é mais rápida. - Reflection sem custo no ORM: metadata de struct é computada uma única vez por tipo e cacheada em
sync.Map— o hot path não faz nenhuma reflection.
Medido em localhost com
SERVER_WORKERS=4, respeitando a configuração do.env. Resultados variam conforme hardware e carga de trabalho da view.
Checklist de produção (segurança)
O bootstrap recusa iniciar em APP_ENV=production se algum segredo estiver
ausente ou for o valor de exemplo. Antes de subir:
APP_ENV=production.SECRET_KEYforte (≥ 32 caracteres).PASSWORD_PEPPERdefinido. Se usar camposkyrux:"encrypt", tambémFIELD_ENCRYPTION_KEY. Gere cada um comopenssl rand -base64 32. SemFIELD_ENCRYPTION_KEYa criptografia de campos falha ao gravar (fail-closed — nunca cifra com chave fraca).ALLOWED_HOSTScom os domínios reais (bloqueia Host header forjado).- Atrás de proxy reverso com TLS: defina
TRUSTED_PROXY_HEADER=X-Forwarded-Fore garanta que o proxy sobrescreve esse header (senão o cliente forja o IP e escapa do RateLimit). Sem proxy, deixe em branco. - Aplique
RateLimitnas rotas sensíveis. O login já tem freio de brute-force embutido (10 falhas/minuto por conta+IP; ajuste comauth.SetLoginThrottle).
Em produção, os headers de segurança (HSTS, CSP, X-Frame-Options DENY, nosniff),
o cookie de sessão Secure e a página de erro sem stack trace são ativados
automaticamente. A debug page só existe em development e apenas para localhost.
Deploy e HTTP/2
O Kyrux serve HTTP/1.1 (sem TLS próprio). O deploy recomendado é atrás de um
proxy reverso (nginx, Caddy, Traefik) que termina TLS e fala HTTP/2/3 com o
browser — o proxy conversa com o Kyrux em HTTP/1.1 com keep-alive, o que não
limita o throughput. Configure TRUSTED_PROXY_HEADER (acima) para o RateLimit
e o throttle de login enxergarem o IP real do cliente.
Sobre SERVER_WORKERS: omita em produção — o runtime Go usa todos os CPUs
por padrão, que é o mais rápido. Defina apenas para limitar consumo de CPU
(ex: máquina compartilhada); valores abaixo do número de CPUs reduzem o throughput.
Health check
O bootstrap já registra GET /health automaticamente — sem tocar banco,
cache ou fila, só confirma que o processo está de pé e respondendo
(200 ok, Cache-Control: no-store). Pronto para o HEALTHCHECK do
Docker ou o probe de liveness de qualquer orquestrador, sem nenhuma
configuração adicional:
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
Rodando os testes de performance
Todos os benchmarks ficam em core/router/benchmark/ e estão organizados em três camadas.
Layer 1 — Microbenchmark (custo de registro de rotas)
go test ./core/router/benchmark/ -bench='^Benchmark(Router|Handle)' -benchmem -benchtime=3s -run='^$'
Mede apenas o custo de registrar rotas via API pública — sem request, sem TCP.
Layer 2 — Framework benchmark (router + middleware, sem TCP)
go test ./core/router/benchmark/ -bench='^Benchmark(Route|Middleware|Parallel)' -benchmem -benchtime=3s -run='^$'
Usa httptest.NewRecorder — elimina overhead de rede. Referência para custo relativo entre tipos de rota e chains de middleware.
Layer 2 — Regressão automática
go test ./core/router/benchmark/ -run TestRegressionCheck -v -count=1
Falha se qualquer cenário regredir mais de 5% em relação ao baseline. Atualizar as constantes em bench_regression_test.go após otimizações intencionais ou troca de hardware.
Layer 3 — Throughput via Go client
# Router puro (sem bootstrap, sem templates)
go test ./core/router/benchmark/ -run TestThroughputRouter -v -count=1
# Stack completo (bootstrap + apps + templates) — requer ao menos um app com rota GET /
go test ./core/router/benchmark/ -run TestThroughputStack -v -count=1
# Banco real + admin autenticado — autocontido, não precisa de nenhum app
# instalado (registra o próprio model, semeia e limpa os dados sozinho)
go test ./core/router/benchmark/ -run TestThroughputReal -v -count=1
Todos sobem um servidor real em porta aleatória, disparam requisições por 5 s com múltiplas goroutines e reportam req/s. TestThroughputStack e TestThroughputReal forçam APP_ENV=production automaticamente.
Nunca usar
./...para os testes de throughput — pacotes rodando em paralelo dividem CPU artificialmente e distorcem os resultados.
Layer 3 — Throughput via ab (capacidade máxima)
# 1. Subir o servidor de benchmark
go run /tmp/kyrux_bench_server.go &
# 2. Rodar os testes
ab -n 50000 -c 100 -k http://127.0.0.1:8000/ping/
ab -n 50000 -c 100 -k http://127.0.0.1:8000/usuarios/42/
ab -n 50000 -c 100 -k "http://127.0.0.1:8000/busca/?q=kyrux&page=3"
ab -n 50000 -c 500 -k http://127.0.0.1:8000/ping/ # teste de pico
# 3. Encerrar o servidor — SEMPRE ao final
kill $(lsof -ti :8000) 2>/dev/null
O servidor de benchmark (/tmp/kyrux_bench_server.go) usa runtime.GOMAXPROCS(4) e registra três rotas (estática, path param, query string). O template completo está em .claude/performance_testing.md.
| Camada | req/s típico | O que mede |
|---|---|---|
| Layer 1 — registro | sub-µs por rota | Custo da primitiva, sem request |
Layer 2 — framework (-bench) |
~433k–592k | Router + handler, sem syscall de rede |
| Layer 2 — regressão | ~620k–1.2M | Mesmo código, contexto mais aquecido |
Layer 3 — Go client (TestThroughputReal) |
~1.900–39.000 (banco real + admin autenticado — ver tabela acima) | Throughput real com overhead de net/http, banco e template |
Layer 3 — ab |
~120k–220k | Capacidade máxima com cliente C otimizado |
Não comparar números entre camadas — cada uma mede uma coisa diferente (processo/cliente, presença de banco, formato de resposta).
Correção: o cliente de
TestThroughputRealfechava o body da resposta sem drenar o conteúdo antes — isso impede onet/httpde reaproveitar a conexão via keep-alive, forçando uma conexão TCP nova a cada requisição (competindo por CPU com o próprio servidor, já que cliente e servidor rodam no mesmo processo/GOMAXPROCS). Corrigido comio.Copy(io.Discard, resp.Body)antes doClose().