---
title: "GOMEMLIMIT em Go: Limite de Memória Soft para Produção"
url: "https://golang.com.br/blog/gomemlimit-go-limite-memoria-producao/"
markdown_url: "https://golang.com.br/blog/gomemlimit-go-limite-memoria-producao.MD"
description: "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."
date: "2026-08-12"
author: "Golang Brasil"
---

# 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](/blog/gomaxprocs-container-aware-go-1-25/): um cuida de **CPU**, o outro de **memória**. Também se conecta a [pprof em produção](/blog/pprof-go-producao/), [go tool trace](/blog/go-tool-trace-runtime-trace-latencia-goroutines/), [Green Tea GC](/blog/green-tea-garbage-collector/), [sync.Pool](/blog/sync-pool-go-reutilizacao-objetos-performance/), [PGO](/blog/go-pgo-profile-guided-optimization-performance/) e ao [deploy no Kubernetes](/tutoriais/go-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”.

| 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 `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](/blog/configuracao-go-viper-variaveis-ambiente/): o mesmo binário, limites diferentes por ambiente.

```bash
# 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:

```yaml
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`:

```go
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:

| 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; 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](/blog/go-pgo-profile-guided-optimization-performance/) 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](/blog/valkey-vs-redis-go-cache-producao/)).

## 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:

```bash
curl -sS "http://127.0.0.1:6060/debug/pprof/heap" -o heap.pb.gz
go tool pprof -http=:8081 heap.pb.gz
```

5. **Olhe goroutines e caches.** Vazamentos clássicos: maps sem eviction, slices que só appendam, subscribers de mensageria sem bound, pools mal usados.
6. **Correlacione com deploy e carga.** OOM só no deploy costuma ser stampede de cache frio — veja [singleflight](/blog/singleflight-go-cache-stampede-deduplicacao/) e [graceful shutdown](/blog/graceful-shutdown-go-producao/).

Para o passo a passo completo de profiling, use o [guia de pprof em produção](/blog/pprof-go-producao/) e o de [benchmarks com benchmem](/blog/benchmarks-go-testing-b-benchmem-pprof/). Para latência de scheduler e STW, [go tool trace](/blog/go-tool-trace-runtime-trace-latencia-goroutines/) e o [flight recorder do Go 1.25](/blog/flight-recorder-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:

```go
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](/blog/health-checks-go-liveness-readiness-startup/).

## 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](/blog/go-pgo-profile-guided-optimization-performance/).

O post oficial traduzido sobre [containers e inteligência de runtime](/blog/go-blog-go-containers-inteligencia-runtime/) e o de [GOMAXPROCS no 1.25](/blog/gomaxprocs-container-aware-go-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](/blog/sync-pool-go-reutilizacao-objetos-performance/).
- **Streaming em vez de carregar tudo.** JSON, uploads e queries grandes: processe em chunks ([encoding/json v2 e streaming](/blog/json-go-encoding-json-v2-streaming-validacao/), [upload multipart](/blog/upload-arquivos-go-multipart-s3-seguranca/)).
- **Worker pools com limite.** Evite fan-out ilimitado de goroutines em picos ([worker pool](/blog/worker-pool-go-fila-jobs/), [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/)).
- **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](/blog/pprof-go-producao/) protegido (não público na internet).
6. [Health checks](/blog/health-checks-go-liveness-readiness-startup/) e [graceful shutdown](/blog/graceful-shutdown-go-producao/) 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](/blog/gomaxprocs-container-aware-go-1-25/).
3. Instrumente memória e GC (Prometheus ou [OpenTelemetry](/blog/go-opentelemetry-observabilidade-tracing-metricas/)).
4. Rode um exercício de carga e capture [pprof](/blog/pprof-go-producao/) no platô.
5. Revise caches locais e combine com [singleflight](/blog/singleflight-go-cache-stampede-deduplicacao/) onde houver stampede.
6. Atualize o runtime e acompanhe melhorias de GC ([Green Tea](/blog/green-tea-garbage-collector/), [Go 1.25+](/blog/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.
