What do runtime/debug.SetMaxStack, SetMaxThreads, SetGCPercent and SetMemoryLimit do, and when would you use them?
Question 575HardGo 1.22 to 1.25
- SetMaxStack(bytes): the maximum size of a single goroutine's stack. The default is 1 GB on 64-bit and 250 MB on 32-bit. Going over it is a fatal "goroutine stack exceeds limit" error, which cannot be recovered. Lower it in tests to catch runaway recursion early.
- SetMaxThreads(n): the maximum number of OS threads. The default is 10000. Going over it crashes the program. Threads pile up because of goroutines blocked in syscalls or cgo, or
LockOSThread, not because of ordinary goroutines. Hitting this limit points to a leak of blocking syscalls. - SetGCPercent(p): the same setting as
GOGC. The next GC starts when the heap grows p% over the live heap. The default is 100, and -1 turns the GC off. The function returns the previous value. - SetMemoryLimit(bytes) (Go 1.19): the same setting as
GOMEMLIMIT. It is a soft limit on total Go runtime memory (heap, stacks and runtime overhead, but not cgo or mmap'd memory). As memory approaches the limit, GC runs more often. The runtime caps GC CPU at about 50% to avoid a death spiral. The default ismath.MaxInt64, meaning no limit.
package main
import (
"fmt"
"runtime/debug"
)
func main() {
fmt.Println(debug.SetGCPercent(50)) // what does this print?
fmt.Println(debug.SetGCPercent(-1))
fmt.Println(debug.SetMemoryLimit(-1) == 1<<63-1) // negative = query only
}
Answer (with GOGC unset): 100, then 50, then true. Each setter returns the previous value. SetMemoryLimit with a negative argument only reads the current limit and changes nothing.
Typical use in production: in a container with a 1 GiB limit, set GOMEMLIMIT=900MiB and leave GOGC at 100, or raise it. This avoids OOM kills from GC headroom while keeping GC rare at low load. GOGC=off combined with a memory limit gives maximum throughput for batch jobs.
Gotcha: if the live heap is truly larger than the limit, the process thrashes in GC and then gets OOM-killed anyway. The limit is not a replacement for fixing memory leaks. Prefer environment variables so operators can tune these without a rebuild.
More on Observability, Debugging & Production Operations
- Q573How do you configure TLS correctly in Go (crypto/tls MinVersion, certificate reloading, mTLS)?
- Q574How do you implement WebSockets in Go? How do you handle concurrent writes, ping/pong keepalives and backpressure?
- Q576How do you monitor Go runtime health in production (runtime/metrics, goroutine counts, GC pause, heap), and what alerts would you set?
- Q577How do you detect and fix high latency caused by lock contention, GC pressure, or connection-pool exhaustion using production telemetry?