← Voltar para o blog

GOMEMLIMIT em Go: Limite de Memória Soft para Produção

GOMEMLIMIT em Go na prática: soft memory limit, GC, OOMKilled no Kubernetes, relação com GOMAXPROCS e como configurar sem matar a latência. Guia de produção.

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:

  1. O live heap cresce com carga (buffers, caches, objetos de request).
  2. O runtime ainda “acha” que pode dobrar o heap (GOGC=100).
  3. O cgroup corta o processo com SIGKILL quando a soma (heap + stacks + runtime + off-heap) passa do limite.
  4. 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”.

SinalSoft limit (GOMEMLIMIT)Hard limit (cgroup / K8s)
Quem aplicaRuntime Go (GC)Kernel / kubelet
Pode ultrapassar?Sim, em picosNão — processo morre
Efeito típicoMais GC, menos RSSOOMKilled
ConfiguraçãoGOMEMLIMIT ou SetMemoryLimitresources.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 map global 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 o GOGC continua 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:

  1. Pegue o limits.memory do container.
  2. Reserve margem para stacks, runtime, buffers de SO e picos de alocação (tipicamente 5–15%).
  3. Coloque GOMEMLIMIT no restante (muitos times usam ~90%).
  4. 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.MaxInt64 equivale a “sem limite prático” (similar a off).
  • Chamar cedo no main evita 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:

KnobPergunta que respondeEfeito 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; mantenha GOGC=100 no 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 GOGC e em PGO se a CPU do GC for o gargalo.
  • GOGC=off com GOMEMLIMIT: 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 OOMKilled sob 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_fraction alto, 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:

  1. Confirme o motivo do restart. No Kubernetes: kubectl describe pod e eventos OOMKilled. No Docker: exit 137.
  2. Compare RSS com o limit. Se RSS colou no hard limit, o soft limit não estava ativo ou estava alto demais.
  3. Verifique se GOMEMLIMIT chegou no processo. kubectl exec + printenv GOMEMLIMIT, ou logue debug.SetMemoryLimit(-1) (consulta) no boot em staging.
  4. 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
  1. Olhe goroutines e caches. Vazamentos clássicos: maps sem eviction, slices que só appendam, subscribers de mensageria sem bound, pools mal usados.
  2. 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 GOMAXPROCS ao CPU limit do cgroup automaticamente; memória ainda exige GOMEMLIMIT explícito (ou SetMemoryLimit).

Ordem de afinação recomendada:

  1. Defina requests/limits realistas no pod.
  2. Configure GOMEMLIMIT ~90% do memory limit.
  3. Confirme GOMAXPROCS (automático no 1.25+, ou explícito em versões anteriores).
  4. Meça p95, RSS e CPU de GC sob carga.
  5. 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.Pool com 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”:

  1. resources.limits.memory definido e realista.
  2. GOMEMLIMIT ≈ 85–95% do limit do container.
  3. GOMAXPROCS coerente com o CPU limit (automático no Go 1.25+).
  4. Métricas de RSS, heap live, contagem de GC e latência p95/p99.
  5. Endpoint ou agente de pprof protegido (não público na internet).
  6. Health checks e graceful shutdown testados no deploy.
  7. Caches e filas com bound explícito.
  8. Teste de carga que aproxima o soft limit sem matar o pod.
  9. Alerta de RSS > 80% do hard limit (antes do OOM).
  10. 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, GOMEMLIMIT mascara o sintoma até a latência ficar inaceitável.

Próximos passos

Se você está endurecendo um serviço Go no Kubernetes:

  1. Defina GOMEMLIMIT em todos os deployments que já têm memory limit.
  2. Alinhe com o GOMAXPROCS container-aware.
  3. Instrumente memória e GC (Prometheus ou OpenTelemetry).
  4. Rode um exercício de carga e capture pprof no platô.
  5. Revise caches locais e combine com singleflight onde houver stampede.
  6. 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.