Kyrux
Voltar

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 em benchmark/).

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/GOMAXPROCS do servidor — por isso os números são mais baixos que a tabela ab acima, que roda em processo separado), APP_ENV=production, Postgres real na mesma máquina. Reproduza com go run main.go benchmark (ou go 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 DELETE do 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-Length nas respostas JSON, que elimina o overhead de chunked transfer encoding no 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_WORKERS atenua 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 development são ~9× mais lentas (renderizam o template HTML de debug completo). Em production, 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:

  1. APP_ENV=production.
  2. SECRET_KEY forte (≥ 32 caracteres). PASSWORD_PEPPER definido. Se usar campos kyrux:"encrypt", também FIELD_ENCRYPTION_KEY. Gere cada um com openssl rand -base64 32. Sem FIELD_ENCRYPTION_KEY a criptografia de campos falha ao gravar (fail-closed — nunca cifra com chave fraca).
  3. ALLOWED_HOSTS com os domínios reais (bloqueia Host header forjado).
  4. Atrás de proxy reverso com TLS: defina TRUSTED_PROXY_HEADER=X-Forwarded-For e garanta que o proxy sobrescreve esse header (senão o cliente forja o IP e escapa do RateLimit). Sem proxy, deixe em branco.
  5. Aplique RateLimit nas rotas sensíveis. O login já tem freio de brute-force embutido (10 falhas/minuto por conta+IP; ajuste com auth.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 TestThroughputReal fechava o body da resposta sem drenar o conteúdo antes — isso impede o net/http de 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 com io.Copy(io.Discard, resp.Body) antes do Close().