How to Set and Read GOMAXPROCS, GOGC, GOTRACEBACK

Set GOMAXPROCS, GOGC, and GOTRACEBACK via environment variables or runtime functions to control CPU usage, garbage collection, and panic tracebacks in Go.

The hidden knobs under the hood

Your Go service runs smoothly on your laptop. You deploy it to a four-core server and the CPU usage spikes to one hundred percent while throughput flatlines. You add a memory-heavy batch job and the garbage collector starts pausing the entire process for half a second every few minutes. You hit a panic in production and the stack trace cuts off exactly where you need it to continue.

These are not bugs in your code. They are symptoms of three runtime knobs that control how Go schedules work, manages memory, and reports failures. The knobs are GOMAXPROCS, GOGC, and GOTRACEBACK. They live in the runtime package and the system environment. Knowing how to read them, when to adjust them, and what happens when you leave them alone separates casual Go developers from people who actually understand the machine.

What these variables actually control

Go does not map goroutines directly to operating system threads. That would be expensive. Instead, the runtime uses a work-stealing scheduler that multiplexes millions of lightweight goroutines across a small pool of OS threads. GOMAXPROCS sets the size of that pool. It tells the scheduler how many OS threads can execute Go code simultaneously. The default matches your logical CPU count. If you run on a machine with eight cores, Go creates eight execution slots by default.

GOGC controls the garbage collector trigger. Go uses a concurrent, tri-color mark-and-sweep collector. It runs while your program is executing, but it still needs to pause the world briefly to synchronize. The collector decides when to start based on heap growth. GOGC is a percentage. A value of one hundred means the collector triggers when the heap doubles since the last collection. A value of twenty means it triggers when the heap grows by twenty percent. Lower values mean more frequent collections and less memory usage. Higher values mean more memory usage and longer pauses.

GOTRACEBACK controls panic verbosity. When a goroutine panics, the runtime prints a stack trace. This variable decides how much of the stack gets printed. The default is crash, which shows Go frames and stops at system boundaries. Setting it to sys or all includes C frames and assembly-level details. This is useful when debugging cgo code or runtime internals, but it floods standard error with noise during normal development.

Think of a restaurant kitchen. GOMAXPROCS is the number of chefs allowed at the stove at once. GOGC is how full the trash bin gets before someone takes it out. GOTRACEBACK is how much detail the manager wants when a dish burns. Change the wrong knob and the kitchen either chokes or wastes resources.

Reading and writing at runtime

You can inspect and modify GOMAXPROCS and GOGC while the program is running. The runtime package exposes functions that read current values and apply new ones. The trick is that passing zero to runtime.GOMAXPROCS() returns the current value instead of changing it. This is a common Go pattern for query functions that double as setters.

Here is the simplest way to read and adjust both values:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// Query current parallelism without changing it
	currentProcs := runtime.GOMAXPROCS(0)
	fmt.Println("Running on", currentProcs, "OS threads")

	// Lower GC threshold to trigger collection sooner
	// Returns the previous percentage for logging or rollback
	oldPercent := runtime.SetGCPercent(50)
	fmt.Printf("GC threshold changed from %d to 50\n", oldPercent)

	// Restore original value if needed later
	runtime.SetGCPercent(oldPercent)
}

The runtime checks environment variables first when the program starts. If GOMAXPROCS is set in the environment, it overrides the CPU count detection. If GOGC is set, it overrides the default one hundred. Runtime calls to GOMAXPROCS() or SetGCPercent() override the environment values for the remainder of the process. The scheduler and collector pick up the new values immediately. There is no restart required.

The runtime/debug package provides a safer way to read these values in production code. It exposes debug.ReadBuildInfo() and debug.SetMaxThreads(), but more importantly, it documents the expected behavior of runtime tuning. The community convention is to read configuration once at startup, log the effective values, and leave them alone. Dynamic tuning during steady-state operation introduces race conditions in your own monitoring logic.

Goroutines are cheap. Scheduler slots are not.

When environment variables take over

GOTRACEBACK cannot be changed at runtime. The runtime reads it exactly once during initialization. If you need more verbose panic output, you must set it before the binary starts. This design prevents accidental information leaks in production and keeps panic handling predictable.

Here is how you typically configure all three in a deployment script:

# Set parallelism to match physical cores, not hyperthreads
export GOMAXPROCS=4

# Aggressive GC for a memory-constrained microservice
export GOGC=20

# Verbose stack traces for debugging cgo boundaries
export GOTRACEBACK=system

# Launch the service
./my-service

The system value for GOTRACEBACK includes Go frames and system frames up to the first C call. The all value prints everything, including assembly registers and kernel boundaries. The crash value is the default and stops at Go boundaries. The none value suppresses the stack trace entirely, which is sometimes used in embedded contexts where standard error is redirected to a watchdog.

You can inspect active values from outside the process using go env or by reading /proc/<pid>/environ on Linux. The runtime package does not export a function to read GOTRACEBACK because it is not stored in a mutable variable. The runtime parses it into internal flags and discards the string. If you need to log the effective panic verbosity, you must capture the environment variable yourself before calling runtime functions.

The compiler rejects programs that try to assign to runtime.GOMAXPROCS directly because it is a function, not a variable. You get runtime.GOMAXPROCS is not a type if you treat it like a field. Always call it with an argument.

Convention aside: the runtime package is part of the standard library, but it is also the foundation of the language. Treat it like a shared library you do not own. Read its documentation, respect its invariants, and never cache its return values across long-running loops. The scheduler can change under your feet.

Pitfalls and silent surprises

Changing GOMAXPROCS while goroutines are blocked on network I/O can cause temporary starvation. The scheduler redistributes work across the new thread pool, but blocked goroutines do not wake up until their I/O completes. If you drop from eight threads to one, every pending network call serializes. The runtime handles this gracefully, but your latency metrics will spike.

Lowering GOGC too aggressively causes GC thrashing. The collector runs so frequently that it spends more time marking and sweeping than your application spends doing useful work. You will see CPU usage stay high while throughput drops. The runtime exposes runtime.MemStats to help you diagnose this. Check the PauseTotalNs and NumGC fields. If NumGC increments rapidly while HeapAlloc stays flat, your threshold is too low.

Setting GOTRACEBACK=all in production floods your logs with assembly frames. Log aggregators choke on the volume. Alerting systems trigger false positives. The runtime prints the trace to standard error, which usually goes to the same file descriptor as your application logs. Filter it or redirect it before it reaches your pipeline.

The runtime also enforces a minimum GOMAXPROCS of one. Passing zero returns the current value. Passing a negative number panics with runtime: GOMAXPROCS should be > 0. The panic is immediate and unrecoverable. The runtime does not silently clamp invalid values. It fails fast so you notice configuration errors during testing.

Memory profiling tools like pprof read GOGC indirectly. If you change the threshold mid-run, the heap profile baseline shifts. Compare profiles taken under identical GC settings. Otherwise you are comparing apples to oranges.

The worst runtime tuning mistake is the one that never shows up in metrics.

Decision matrix: when to touch what

Use environment variables when you need to configure the runtime before the binary starts. Use GOMAXPROCS in the environment when your container orchestrator limits CPU shares and you want the scheduler to respect the cgroup boundary. Use GOGC in the environment when your deployment pipeline handles configuration and you want reproducible builds. Use GOTRACEBACK in the environment when you need verbose panic output for debugging and cannot modify the source code.

Use runtime.GOMAXPROCS() with a positive argument when you need to dynamically adjust parallelism based on load. Use runtime.SetGCPercent() when your workload shifts between CPU-bound and memory-bound phases and you want to tune collection frequency without restarting. Use runtime/debug functions when you need to read runtime state safely in production code. Use plain sequential configuration at startup when you want predictable behavior and minimal operational complexity.

Leave GOMAXPROCS alone when your application runs on dedicated hardware with stable CPU counts. Leave GOGC at one hundred when your service has plenty of memory and you prefer longer pauses over frequent collection. Leave GOTRACEBACK at crash when you run in production and want clean logs. Leave all three untouched when you are writing a library that other teams will embed.

Trust the defaults. Tune only when metrics tell you to.

Where to go next