Resposta rápida: GOMEMLIMIT é o soft memory limit do runtime Go. Em containers e Kubernetes, ele orienta o garbage collector a trabalhar mais cedo quando o processo se aproxima de um teto de memória, reduzindo a chance de OOMKilled. Não substitui o memory limit do pod: o padrão seguro é colocar GOMEMLIMIT em cerca de 90% do limite do container e combinar com métricas, pprof e um GOMAXPROCS coerente com o CPU limit.
Se você já viu um pod Go saudável em CPU, com latência estável, e de repente o kubelet registra OOMKilled, o problema quase nunca é “falta de GC”. É desalinhamento entre o que o runtime acha que pode usar e o hard limit do cgroup. Desde o Go 1.19 o runtime expõe um soft limit (GOMEMLIMIT / debug.SetMemoryLimit) justamente para esse cenário.
Este guia fecha o par natural com o GOMAXPROCS container-aware do Go 1.25: um cuida de CPU, o outro de memória. Também se conecta a pprof em produção, go tool trace, Green Tea GC, sync.Pool, PGO e ao deploy no Kubernetes.
Por que serviços Go morrem com OOMKilled
O GC do Go é eficiente, mas o padrão histórico otimiza throughput: o heap cresce até um múltiplo do live set (controlado por GOGC) e só então o coletor age com mais força. Em uma máquina bare-metal com dezenas de gigabytes livres, isso é ótimo. Em um pod com memory: 512Mi, o mesmo comportamento empurra o processo contra o hard limit do cgroup.
O que acontece na prática:
- O live heap cresce com carga (buffers, caches, objetos de request).
- O runtime ainda “acha” que pode dobrar o heap (
GOGC=100). - O cgroup corta o processo com SIGKILL quando a soma (heap + stacks + runtime + off-heap) passa do limite.
- O pod reinicia. Dependendo do probe, o tráfego volta e o ciclo se repete.
Sem soft limit, o GC só reage depois de alocar. Com soft limit, o runtime tenta liberar memória antes de estourar o teto. É a diferença entre “quase OOM, GC corre” e “OOM, processo morto”.
| Sinal | Soft limit (GOMEMLIMIT) | Hard limit (cgroup / K8s) |
|---|---|---|
| Quem aplica | Runtime Go (GC) | Kernel / kubelet |
| Pode ultrapassar? | Sim, em picos | Não — processo morre |
| Efeito típico | Mais GC, menos RSS | OOMKilled |
| Configuração | GOMEMLIMIT ou SetMemoryLimit | resources.limits.memory |
O que GOMEMLIMIT é (e o que não é)
GOMEMLIMIT define um alvo de memória total que o runtime tenta respeitar. Internamente isso alimenta a política do GC: quando o uso se aproxima do limite, o coletor fica mais agressivo, mesmo que GOGC sozinho ainda não disparasse uma coleta “cheia”.
Pontos que evitam confusão:
- Não é um hard cap. O processo ainda pode alocar além do valor se o live set exigir. O soft limit não impede alocação; ele incentiva liberação.
- Não conta só o heap Go. O limite considera a memória total que o runtime estima para o processo (incluindo overhead do runtime). Por isso o valor deve ficar abaixo do limite do container.
- Não corrige vazamento. Objetos alcançáveis continuam vivos. Se um
mapglobal só cresce, o GC não “salva” o pod. - Não substitui
GOGC. Os dois cooperam. Em muitos serviços em container, o soft limit é o freio de segurança e oGOGCcontinua afinando o custo de CPU do GC.
Introduzido no Go 1.19, o soft memory limit é estável e está no caminho crítico de qualquer serviço containerizado moderno. Se o seu binário ainda roda em versão anterior, atualize: o ganho operacional sozinho já paga o upgrade.
Como configurar: ambiente primeiro
A forma preferida em produção é a variável de ambiente. Ela respeita o twelve-factor: o mesmo binário, limites diferentes por ambiente.
# Exemplos válidos
export GOMEMLIMIT=512MiB
export GOMEMLIMIT=1536MiB
export GOMEMLIMIT=2GiB
export GOMEMLIMIT=off # desliga o soft limit (padrão histórico)
Sufixos aceitos: B, KiB, MiB, GiB (base 1024). Sem sufixo, o valor é em bytes. off remove o limite.
Em Kubernetes, o padrão mais limpo é o env do container:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-go
spec:
template:
spec:
containers:
- name: api
image: registry.example.com/api-go:1.26
resources:
requests:
cpu: "500m"
memory: 512Mi
limits:
cpu: "1"
memory: 512Mi
env:
- name: GOMEMLIMIT
value: "460MiB" # ~90% de 512Mi
# GOMAXPROCS no Go 1.25+ já lê o CPU limit do cgroup
Regra prática de dimensionamento:
- Pegue o
limits.memorydo container. - Reserve margem para stacks, runtime, buffers de SO e picos de alocação (tipicamente 5–15%).
- Coloque
GOMEMLIMITno restante (muitos times usam ~90%). - Monitore RSS real e ajuste: se o GC dispara demais e a latência p95 sobe, o limite está apertado demais ou o live heap é grande demais para o pod.
Configuração em código com runtime/debug
Quando o limite precisa ser calculado em runtime (por exemplo, lendo cgroup v2 ou um arquivo de config), use debug.SetMemoryLimit:
package main
import (
"log"
"os"
"runtime/debug"
"strconv"
)
func configureMemoryLimit() {
// Preferência: GOMEMLIMIT já lido pelo runtime na subida do processo.
// Use SetMemoryLimit só quando o valor vem de outra fonte.
raw := os.Getenv("APP_MEM_LIMIT_BYTES")
if raw == "" {
return
}
n, err := strconv.ParseInt(raw, 10, 64)
if err != nil || n <= 0 {
log.Printf("APP_MEM_LIMIT_BYTES inválido: %q", raw)
return
}
previous := debug.SetMemoryLimit(n)
log.Printf("memory limit: %d bytes (anterior=%d)", n, previous)
}
func main() {
configureMemoryLimit()
// sobe servidor HTTP, workers, etc.
}
Observações de API:
debug.SetMemoryLimit(limit int64)devolve o limite anterior.- Passe um valor em bytes.
math.MaxInt64equivale a “sem limite prático” (similar aoff).- Chamar cedo no
mainevita uma janela em que o processo sobe sem freio.
Para a maioria dos deploys, não escreva esse código: configure GOMEMLIMIT no manifesto e deixe o runtime aplicar sozinho.
GOGC e GOMEMLIMIT juntos
Os dois knobs respondem perguntas diferentes:
| Knob | Pergunta que responde | Efeito principal |
|---|---|---|
GOGC | “Quanto o heap pode crescer em relação ao live set?” | Frequência relativa do GC |
GOMEMLIMIT | “Qual o teto absoluto que tentamos respeitar?” | Pressão de memória sob container |
Padrões úteis:
- Serviço em pod pequeno (256–512Mi):
GOMEMLIMIT~90% do limit; mantenhaGOGC=100no início e só mude com dados. - Batch / worker com picos de alocação: soft limit evita OOM no pico; se o GC roubar CPU demais, aumente o pod em vez de desligar o limite.
- Máquina dedicada sem hard limit: soft limit é opcional; foque em
GOGCe em PGO se a CPU do GC for o gargalo. GOGC=offcomGOMEMLIMIT: modo avançado em que o soft limit passa a ser o principal gatilho de coleta. Só use com observabilidade madura — sem métricas, você troca um problema (OOM) por outro (latência opaca).
Evite a armadilha de “desligar o GC” em produção. Em Go, o caminho profissional é limitar memória de forma consciente, não torcer para o live set caber no pod.
Sintomas de limite mal calibrado
Limite alto demais (ou ausente)
- RSS sobe até o hard limit do cgroup.
- Restarts com
OOMKilledsob pico de tráfego ou deploy. - Heap profiles mostram crescimento “normal” até o kill — não há plateau.
Limite baixo demais
- GC quase contínuo (
gc_cpu_fractionalto, ou métricas OTel/Prometheus de GC frequentes). - Latência p95/p99 sobe sem mudança de QPS.
- Throughput cai em workers CPU-bound.
- Logs de “GC forced” / alocações que falham em liberar o suficiente (live set > soft limit).
Live set maior que o soft limit
Se a aplicação precisa de 700Mi de objetos vivos e o GOMEMLIMIT é 460Mi, o runtime não tem mágica: ele coleta o que pode e o processo continua sob pressão. A correção é aumentar o pod, reduzir caches em memória, ou externalizar estado (Redis/Valkey, ver Valkey vs Redis em Go).
Diagnóstico: do OOM ao heap profile
Checklist curto quando um serviço Go reinicia por memória:
- Confirme o motivo do restart. No Kubernetes:
kubectl describe pode eventosOOMKilled. No Docker: exit 137. - Compare RSS com o limit. Se RSS colou no hard limit, o soft limit não estava ativo ou estava alto demais.
- Verifique se
GOMEMLIMITchegou no processo.kubectl exec+printenv GOMEMLIMIT, ou loguedebug.SetMemoryLimit(-1)(consulta) no boot em staging. - Capture heap com pprof antes do kill, se possível:
curl -sS "http://127.0.0.1:6060/debug/pprof/heap" -o heap.pb.gz
go tool pprof -http=:8081 heap.pb.gz
- Olhe goroutines e caches. Vazamentos clássicos: maps sem eviction, slices que só appendam, subscribers de mensageria sem bound, pools mal usados.
- Correlacione com deploy e carga. OOM só no deploy costuma ser stampede de cache frio — veja singleflight e graceful shutdown.
Para o passo a passo completo de profiling, use o guia de pprof em produção e o de benchmarks com benchmem. Para latência de scheduler e STW, go tool trace e o flight recorder do Go 1.25 completam o kit.
Exemplo: API HTTP com limite e health checks
Um esqueleto mínimo que combina soft limit, métricas de runtime e readiness:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"runtime"
"syscall"
"time"
)
func main() {
// GOMEMLIMIT vem do ambiente; não hardcode o valor no binário.
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
// readiness real: ping no banco, fila, etc.
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /debug/mem", func(w http.ResponseWriter, r *http.Request) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// em produção, prefira métricas Prometheus/OTel a endpoint ad hoc
log.Printf("alloc=%d sys=%d numgc=%d", m.Alloc, m.Sys, m.NumGC)
w.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
log.Printf("listening on %s (GOMEMLIMIT=%q)", srv.Addr, os.Getenv("GOMEMLIMIT"))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
}
Note o uso de GET /healthz com o roteamento do ServeMux moderno. Health separado de readiness evita que o kubelet derrube o pod no meio de um GC pesado só porque um dependency check falhou — detalhe no guia de health checks.
Relação com GOMAXPROCS e containers
CPU e memória se cruzam:
- CPU limit baixo + GC agressivo (soft limit apertado) = latência pior, throttling e filas internas.
- CPU folgado + memória sem soft limit = OOM sob pico de alocação.
- Go 1.25+ ajusta
GOMAXPROCSao CPU limit do cgroup automaticamente; memória ainda exigeGOMEMLIMITexplícito (ouSetMemoryLimit).
Ordem de afinação recomendada:
- Defina
requests/limitsrealistas no pod. - Configure
GOMEMLIMIT~90% do memory limit. - Confirme
GOMAXPROCS(automático no 1.25+, ou explícito em versões anteriores). - Meça p95, RSS e CPU de GC sob carga.
- Só então mexa em
GOGC, pools (sync.Pool) e PGO.
O post oficial traduzido sobre containers e inteligência de runtime e o de GOMAXPROCS no 1.25 cobrem o lado de CPU em profundidade.
Padrões que reduzem pressão de memória (além do limite)
GOMEMLIMIT é cinto de segurança, não arquitetura. Combine com hábitos de código:
- Bounds em caches e filas. Todo cache in-process precisa de teto e eviction.
sync.Poolcom cuidado. Ajuda em buffers quentes, mas não é cache semântico — veja o guia de sync.Pool.- Streaming em vez de carregar tudo. JSON, uploads e queries grandes: processe em chunks (encoding/json v2 e streaming, upload multipart).
- Worker pools com limite. Evite fan-out ilimitado de goroutines em picos (worker pool, errgroup).
- Reuse de buffers em I/O.
bytes.Buffer/ slices com[:0]reduzem alocações sem esconder vazamentos. - Offload de estado quente. Cache compartilhado (Valkey/Redis) e filas externas tiram pressão do heap do pod.
Checklist de produção
Antes de considerar o serviço “pronto para o cluster”:
resources.limits.memorydefinido e realista.GOMEMLIMIT≈ 85–95% do limit do container.GOMAXPROCScoerente com o CPU limit (automático no Go 1.25+).- Métricas de RSS, heap live, contagem de GC e latência p95/p99.
- Endpoint ou agente de pprof protegido (não público na internet).
- Health checks e graceful shutdown testados no deploy.
- Caches e filas com bound explícito.
- Teste de carga que aproxima o soft limit sem matar o pod.
- Alerta de RSS > 80% do hard limit (antes do OOM).
- Runbook: “OOMKilled → verificar
GOMEMLIMIT→ heap profile → aumentar pod ou cortar live set”.
Quando não usar (ou usar com cuidado)
- CLIs de curta duração. O processo termina rápido; soft limit raramente importa.
- Jobs com live set legítimo maior que o pod. Aumente o recurso; não aperte o soft limit até o GC derreter a CPU.
- Ambientes sem hard limit e com memória abundante. O ganho é pequeno; foque em profiling de alocação se CPU de GC for o tema.
- Como “fix” de vazamento. Se o heap live só sobe,
GOMEMLIMITmascara o sintoma até a latência ficar inaceitável.
Próximos passos
Se você está endurecendo um serviço Go no Kubernetes:
- Defina
GOMEMLIMITem todos os deployments que já têm memory limit. - Alinhe com o GOMAXPROCS container-aware.
- Instrumente memória e GC (Prometheus ou OpenTelemetry).
- Rode um exercício de carga e capture pprof no platô.
- Revise caches locais e combine com singleflight onde houver stampede.
- Atualize o runtime e acompanhe melhorias de GC (Green Tea, Go 1.25+).
GOMEMLIMIT não deixa o serviço “usar menos memória” por mágica. Ele alinha o comportamento do GC com o contrato de memória do container. Junto com limites honestos no Kubernetes, GOMAXPROCS correto e profiling regular, elimina uma classe inteira de restarts noturnos que parecem aleatórios — e quase sempre são só um soft limit que ninguém configurou.
Perguntas frequentes
O que é GOMEMLIMIT em Go?
GOMEMLIMIT é o soft memory limit do runtime Go. Ele orienta o coletor de lixo a manter o uso de memória do processo abaixo de um teto configurável, liberando heap com mais agressividade quando a aplicação se aproxima do limite. Não é um hard cap do kernel: o processo ainda pode ultrapassar o valor em picos e ser morto pelo OOM killer se o limite do container for estourado.
GOMEMLIMIT substitui o memory limit do Kubernetes?
Não. O memory limit do pod é um hard limit do cgroup. GOMEMLIMIT é um sinal para o GC do Go. O padrão recomendado é definir GOMEMLIMIT um pouco abaixo do memory limit do container (por exemplo 90%) para que o runtime tente liberar memória antes do OOMKilled.
Qual a diferença entre GOGC e GOMEMLIMIT?
GOGC controla o quanto o heap pode crescer em relação ao live heap antes do próximo GC (padrão 100 = dobra). GOMEMLIMIT define um teto absoluto de memória que o runtime tenta respeitar. Em containers com limite fixo, os dois se complementam: GOGC otimiza throughput; GOMEMLIMIT protege contra OOM.
Como configurar GOMEMLIMIT em Go?
Por variável de ambiente (GOMEMLIMIT=512MiB) ou em código com runtime/debug.SetMemoryLimit. Prefira a variável de ambiente em produção: o mesmo binário roda em vários ambientes e o limite acompanha o deploy. Valores aceitam sufixos B, KiB, MiB, GiB e a forma ilimitada off.
GOMEMLIMIT resolve vazamento de memória?
Não. Se a aplicação retém objetos vivos (maps que só crescem, caches sem eviction, goroutines presas), o GC não consegue liberar essa memória. O soft limit só acelera coletas do que já é lixo. Vazamento exige pprof, análise de heap e correção do código que retém referências.