---
title: "pprof em Go: Profiling Seguro em Produção"
url: "https://golang.com.br/blog/pprof-go-producao/"
markdown_url: "https://golang.com.br/blog/pprof-go-producao.MD"
description: "Aprenda a usar pprof em Go para investigar CPU, memória, goroutines, locks e latência em produção sem expor dados sensíveis nem derrubar o serviço."
date: "2026-06-14"
author: "Golang Brasil"
---

# pprof em Go: Profiling Seguro em Produção

Aprenda a usar pprof em Go para investigar CPU, memória, goroutines, locks e latência em produção sem expor dados sensíveis nem derrubar o serviço.


`pprof` é uma das ferramentas mais úteis do ecossistema Go porque transforma suspeita em evidência. Em vez de discutir se a API está lenta por causa de JSON, banco, garbage collector, locks ou excesso de goroutines, você coleta um perfil e olha onde o programa realmente gasta CPU, aloca memória ou fica bloqueado.

O erro comum é tratar `pprof` como recurso de laboratório: algo que só aparece em benchmark local, fora do caminho real de produção. Em serviços Go, muitos problemas só aparecem com tráfego real, dados reais, cache quente, limites de conexão, filas, concorrência e deploy em container. Por isso vale aprender a expor profiling com segurança, coletar perfis curtos, interpretar sinais e fechar o ciclo com correção, benchmark e rollout.

Este guia mostra como usar `pprof` em Go para investigar CPU, heap, goroutines, mutex, block profile e traces sem abrir um painel perigoso na internet. Ele complementa [PGO em Go](/blog/go-pgo-profile-guided-optimization-performance/), [OpenTelemetry em Go](/blog/go-opentelemetry-observabilidade-tracing-metricas/), [health checks em Go](/blog/health-checks-go-liveness-readiness-startup/), [context e timeouts](/blog/context-timeout-cancelamento-go/) e [graceful shutdown](/blog/graceful-shutdown-go-producao/).

## O que é pprof

`pprof` é o formato e o conjunto de ferramentas usados pelo Go para coletar e analisar perfis de execução. A biblioteca padrão oferece dois pacotes principais:

- `runtime/pprof`, para gerar perfis programaticamente;
- `net/http/pprof`, para expor endpoints HTTP de profiling.

Na prática, a maioria dos serviços começa com `net/http/pprof` em uma porta administrativa interna. A partir dela você coleta perfis com `go tool pprof`, `curl` ou ferramentas de automação.

Os perfis mais usados são:

| Perfil | Pergunta que responde |
|---|---|
| CPU | Onde o processo gasta tempo de CPU? |
| Heap | Quais caminhos alocam ou retêm memória? |
| Goroutine | Quais goroutines existem e onde estão paradas? |
| Mutex | Onde goroutines esperam por locks? |
| Block | Onde goroutines bloqueiam em channel, select, cond ou sync? |
| Trace | Como scheduler, GC, syscalls e goroutines interagem ao longo do tempo? |

`pprof` não substitui métricas, logs ou tracing distribuído. Ele é uma lupa. Métricas dizem que CPU subiu, latência piorou ou goroutines cresceram. `pprof` ajuda a explicar por quê.

## Segurança: nunca exponha pprof publicamente

Endpoints de profiling podem revelar nomes de funções, caminhos internos, padrões de tráfego, labels, bibliotecas, endpoints administrativos e até detalhes sensíveis dependendo do código. Além disso, alguns perfis custam CPU e memória. Expor `/debug/pprof` na internet é convite para vazamento e abuso.

Regra prática: `pprof` deve ficar atrás de pelo menos uma dessas proteções:

1. Porta ligada apenas em `127.0.0.1`.
2. Porta administrativa acessível só pela rede interna ou VPN.
3. Autenticação forte no proxy interno.
4. Port-forward temporário via Kubernetes.
5. Feature flag operacional com janela curta.

Evite colocar `pprof` no mesmo servidor público da API sem controle de rota. O exemplo inseguro abaixo é comum em tutoriais, mas não deve ir cru para produção:

```go
import _ "net/http/pprof"

func main() {
    http.ListenAndServe(":8080", nil) // ruim se esse mux também é público
}
```

Prefira separar o servidor administrativo:

```go
package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
)

func main() {
    go func() {
        mux := http.NewServeMux()
        mux.HandleFunc("/debug/pprof/", http.DefaultServeMux.ServeHTTP)
        mux.HandleFunc("/debug/pprof/cmdline", http.DefaultServeMux.ServeHTTP)
        mux.HandleFunc("/debug/pprof/profile", http.DefaultServeMux.ServeHTTP)
        mux.HandleFunc("/debug/pprof/symbol", http.DefaultServeMux.ServeHTTP)
        mux.HandleFunc("/debug/pprof/trace", http.DefaultServeMux.ServeHTTP)

        srv := &http.Server{
            Addr:    "127.0.0.1:6060",
            Handler: mux,
        }
        log.Printf("pprof admin server: %v", srv.ListenAndServe())
    }()

    // servidor público da aplicação em outra porta/mux
}
```

Em Kubernetes, esse desenho permite usar `kubectl port-forward` para uma coleta temporária:

```bash
kubectl port-forward deploy/minha-api 6060:6060
```

Depois colete do seu terminal local usando `localhost`.

## Perfil de CPU

O CPU profile responde onde o programa gastou tempo executando. É o primeiro perfil a coletar quando há consumo alto de CPU, queda de throughput ou aumento de latência sem sinal claro de banco ou rede.

Com o servidor administrativo ativo:

```bash
go tool pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
```

Dentro do console:

```text
(pprof) top
(pprof) list MinhaFuncao
(pprof) web
(pprof) png > cpu.png
```

Também dá para salvar o arquivo bruto:

```bash
curl -o cpu.pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=60"
go tool pprof ./bin/app cpu.pprof
```

Dicas para um perfil de CPU útil:

- Colete durante o problema ou durante carga representativa.
- Use 30 a 120 segundos; perfis muito curtos podem mostrar ruído.
- Compare antes/depois da correção.
- Não tire conclusão de uma coleta isolada se o tráfego varia muito.
- Guarde o commit, versão da imagem e janela de coleta junto do arquivo.

Se `top` mostra muito tempo em serialização JSON, compressão, regex, template, hashing, parse de data ou reflection, há chance de otimização direta. Se mostra chamadas de runtime, GC ou locks, combine com heap, mutex e block profiles antes de mexer.

## Heap profile: alocação versus retenção

O heap profile ajuda a entender memória. Mas existe uma pegadinha: você precisa separar "quem aloca muito" de "quem mantém memória viva".

Coleta básica:

```bash
go tool pprof "http://127.0.0.1:6060/debug/pprof/heap"
```

No console, veja memória em uso:

```text
(pprof) top
(pprof) top -cum
```

Para investigar alocações acumuladas, use `alloc_space`:

```text
(pprof) sample_index=alloc_space
(pprof) top
```

Sinais comuns:

- `inuse_space` alto: objetos ainda vivos, possível cache, mapa global, fila, buffer ou leak.
- `alloc_space` alto mas `inuse_space` baixo: muita alocação temporária, pode pressionar GC sem reter memória.
- Crescimento contínuo de heap junto com goroutines: investigue goroutine leak, canais e contextos sem cancelamento.
- Muito `[]byte` ou `string`: revise cópias, buffers, parsing, compressão e payloads.

Antes de "otimizar" alocação, confirme impacto em métrica. Reduzir alocação em código frio raramente paga. Reduzir alocação em hot path de API ou worker de alto volume pode melhorar CPU, latência e pausas de GC.

## Goroutine profile

O goroutine profile mostra stacks das goroutines existentes. Ele é valioso quando a métrica `go_goroutines` cresce sem estabilizar, quando há shutdown travado, deadlock parcial, consumo de memória inesperado ou workers parados.

```bash
go tool pprof "http://127.0.0.1:6060/debug/pprof/goroutine"
```

Para ver stacks legíveis pelo endpoint direto:

```bash
curl "http://127.0.0.1:6060/debug/pprof/goroutine?debug=2" > goroutines.txt
```

Procure padrões:

- Muitas goroutines presas em receive de channel sem produtor.
- Muitas goroutines esperando `time.After` em loop.
- Chamadas HTTP sem timeout.
- Workers aguardando fila que nunca fecha.
- Goroutines por request que não respeitam `context.Context`.
- Leitores de body, rows ou streams que não são fechados.

Um exemplo clássico de vazamento:

```go
func handler(w http.ResponseWriter, r *http.Request) {
    ch := make(chan Result)

    go func() {
        ch <- callSlowDependency() // pode bloquear para sempre se o handler retornar
    }()

    select {
    case result := <-ch:
        writeResult(w, result)
    case <-time.After(200 * time.Millisecond):
        http.Error(w, "timeout", http.StatusGatewayTimeout)
    }
}
```

O timeout encerra o handler, mas a goroutine pode ficar presa tentando enviar no channel. Melhor usar contexto, buffer pequeno ou desenho em que a goroutine consegue desistir:

```go
func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 200*time.Millisecond)
    defer cancel()

    result, err := callSlowDependency(ctx)
    if err != nil {
        http.Error(w, err.Error(), http.StatusGatewayTimeout)
        return
    }
    writeResult(w, result)
}
```

## Mutex e block profiles

Por padrão, Go não coleta todos os detalhes de mutex e bloqueios porque isso tem custo. Para investigar contenção, habilite amostragem explicitamente:

```go
import "runtime"

func enableProfiles() {
    runtime.SetMutexProfileFraction(10) // 1 em cada 10 eventos de contenção
    runtime.SetBlockProfileRate(10000)  // amostragem em nanossegundos
}
```

Depois colete:

```bash
go tool pprof "http://127.0.0.1:6060/debug/pprof/mutex"
go tool pprof "http://127.0.0.1:6060/debug/pprof/block"
```

Use esses perfis quando CPU não explica a lentidão, mas requisições ficam paradas, workers não avançam ou a latência cresce com concorrência.

Causas comuns:

- `sync.Mutex` global protegendo cache grande.
- Logger síncrono ou writer compartilhado.
- Pool pequeno demais para banco ou HTTP.
- Channel usado como fila sem consumidores suficientes.
- `singleflight` segurando trabalho pesado por chave ampla demais.
- Seção crítica fazendo I/O.

A correção raramente é "remover lock". Normalmente é reduzir escopo da seção crítica, separar sharding por chave, trocar estrutura de dados, mover I/O para fora do lock ou controlar concorrência de forma explícita.

## Trace: quando pprof não basta

O execution trace mostra scheduler, goroutines, syscalls, rede, GC e eventos ao longo do tempo. Ele é mais pesado e mais detalhado do que os perfis tradicionais, mas ajuda em problemas de latência, starvation, pausas e interação entre goroutines.

Coleta curta:

```bash
curl -o trace.out "http://127.0.0.1:6060/debug/pprof/trace?seconds=5"
go tool trace trace.out
```

Use trace com parcimônia. Cinco a dez segundos sob carga representativa costumam ser suficientes. Trace longo vira arquivo grande e difícil de analisar.

## Um fluxo de investigação saudável

Um bom fluxo evita mudanças aleatórias:

1. Comece pela métrica: CPU, memória, latência, goroutines, erros ou throughput.
2. Defina uma hipótese simples.
3. Colete o perfil certo durante uma janela representativa.
4. Salve comando, horário, versão e arquivo.
5. Analise `top`, `top -cum`, `list` e gráfico.
6. Faça uma mudança pequena.
7. Rode testes e benchmark.
8. Recolete perfil no mesmo cenário.
9. Publique com rollout controlado.
10. Remova ou feche acesso temporário se ele foi aberto só para a investigação.

Exemplo: a API está com CPU alta. Você coleta CPU profile e vê 35% em uma função que monta resposta JSON com reflection e conversão repetida de datas. Corrige cache local do formato, adiciona benchmark do encoder, mede redução de alocação e recolhe perfil. Esse é um ciclo fechado.

Contraexemplo: a API está lenta, alguém troca framework, muda pool, ativa cache global e aumenta CPU limit no mesmo deploy. Se melhorar, ninguém sabe por quê. Se piorar, ninguém sabe o que reverter.

## pprof e PGO

Desde Go 1.21, perfis de CPU podem alimentar PGO, a otimização guiada por perfil. O fluxo é parecido, mas a exigência de qualidade é maior: o perfil usado no build precisa representar tráfego real e estável.

```bash
curl -o default.pgo "http://127.0.0.1:6060/debug/pprof/profile?seconds=60"
go build -pgo=default.pgo -o bin/app ./cmd/app
```

Não use um perfil coletado durante incidente, carga artificial ruim ou endpoint administrativo raro para otimizar o binário inteiro. Para detalhes de validação, veja o guia de [PGO em Go](/blog/go-pgo-profile-guided-optimization-performance/).

## Checklist de produção

Antes de deixar `pprof` disponível em um serviço Go, revise:

- `/debug/pprof` não está exposto publicamente.
- A porta administrativa usa `127.0.0.1`, rede interna, VPN ou proxy autenticado.
- Coletas têm duração limitada.
- Perfis salvos não vão para repositório público.
- O time sabe associar perfil a versão/commit.
- Mutex e block profiles só são habilitados quando necessários.
- A coleta não compete com incidentes críticos sem coordenação.
- Perfis usados para PGO passam por revisão.
- Dashboards acompanham CPU, heap, GC, goroutines e latência.
- Correções são validadas com teste, benchmark ou nova coleta.

## Erros comuns

**Expor pprof junto da API pública.** Mesmo que não exista link, scanners encontram endpoints conhecidos.

**Coletar perfil fora do problema.** Perfil local em laptop não explica necessariamente produção em container.

**Confundir alocação acumulada com memória retida.** `alloc_space` alto e `inuse_space` alto contam histórias diferentes.

**Ignorar contexto e timeout.** Muitos leaks de goroutine vêm de chamadas sem cancelamento.

**Otimizar função fria.** Uma função feia mas rara pode aparecer no código e não aparecer no perfil. O perfil manda.

**Mudar muita coisa de uma vez.** Profiling bom reduz incerteza; deploy caótico aumenta.

## Conclusão

`pprof` é uma vantagem prática de Go: está na biblioteca padrão, conversa com o runtime e funciona em serviços reais. O ganho vem de usar a ferramenta com disciplina operacional. Proteja o endpoint, colete perfis curtos e representativos, interprete CPU, heap e goroutines no contexto das métricas e valide cada correção.

Quando esse hábito entra no time, performance deixa de ser debate abstrato. Você passa a ter evidência para decidir se o gargalo está em alocação, lock, goroutine vazando, JSON, banco, GC ou simplesmente em uma hipótese errada.
