sysmon
Before You Start
- Applies to: Go 1.21+ (examples verified with Go 1.22) · Prerequisites: GMP Scheduler · Read time: about 10 min
sysmon is a common function, literally translated as system monitor. Removing the comment parts, it's only about 200 lines of code. It's assigned a separate thread to start during the program bootstrap phase, and then continuously monitors the state of the Go program runtime in the background and makes corresponding processing. The code for its startup can be viewed in the runtime.main function:
func main() {
...
mp := getg().m
mainStarted = true
systemstack(func() {
newm(sysmon, nil, -1)
})
...
}The system monitor itself is just a for loop. The interval between each round of loops is 20 microseconds, and as the program idle index rises, the interval time increases up to a maximum of 10 milliseconds. In each round of the loop, it mainly does the following things:
- Assists goroutine scheduling, preempts long-running goroutines
- Checks memory conditions and determines whether garbage collection is needed
- Checks network poller conditions and handles timed waiting goroutines
- Scavenges unused memory and returns it to the operating system
- Flushes heap profile information
- Drops mcache buffers
Preemption
Preemption work is completed by the runtime.preemptone function. It mainly checks whether the current goroutine's running time exceeds the limit. If so, it sets the goroutine's preempt field to true to indicate it needs to be preempted.
func preemptone(pp *p) bool {
mp := pp.m.ptr()
if mp == nil || mp == getg().m {
return false
}
gp := mp.curg
if gp == nil || gp == mp.g0 {
return false
}
gp.preempt = true
// Every call in a goroutine should check for preemption now.
gp.stackguard0 = stackPreempt
return true
}The goroutine's stackguard0 field is used to check whether preemption is needed. When it's set to stackPreempt, it indicates the goroutine needs to be preempted. This is the mechanism for stack expansion checks. When a goroutine's stack needs to expand, it sets stackguard0 to stackPreempt. When the goroutine calls a function, it checks whether stackguard0 is stackPreempt. If so, it triggers stack expansion. But if the goroutine doesn't call functions for a long time, it can't trigger stack expansion checks. So the system monitor needs to check this situation.
func goschedImpl(gp *g, inheritTime bool) {
...
casgstatus(gp, _Grunning, _Grunnable)
if !inheritTime {
gp.schedtick++
}
globrunq.put(gp, 0)
schedule()
}When a goroutine is preempted, its status is changed from _Grunning to _Grunnable, then it's put into the global runnable queue to wait for rescheduling.
Garbage Collection
The system monitor periodically checks whether garbage collection is needed. If the time since the last garbage collection exceeds the limit, it forcibly triggers garbage collection. This time limit is determined by the runtime.forcegcperiod constant, which is 2 minutes.
func sysmon() {
...
for {
...
// check if we need to force a GC
if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && forcegc.idle.Load() {
lock(&forcegc.lock)
forcegc.idle.Store(false)
var list gList
list.push(forcegc.g)
injectglist(&list)
unlock(&forcegc.lock)
}
...
}
}When garbage collection is triggered, the forcegc goroutine is awakened to execute garbage collection work.
Network Poller
The system monitor also periodically checks the network poller's conditions. If there are timed waiting goroutines, it handles them. This part of the work is completed by the runtime.netpoll function.
func sysmon() {
...
for {
...
// poll network
npoll := netpoll(-1)
if npoll != 0 {
npollcnt++
}
...
}
}When the network poller has timed waiting goroutines, they are awakened to continue execution.
Memory Scavenge
The system monitor also periodically scavenges unused memory and returns it to the operating system. This part of the work is completed by the runtime.mheap.scavenge function.
func sysmon() {
...
for {
...
// scavenge heap
if scavengeHeap() {
scavengeCnt++
}
...
}
}When there's unused memory in the heap, it's returned to the operating system to reduce memory usage.
Other Work
The system monitor also does some other work, such as flushing heap profile information, dropping mcache buffers, etc. These tasks are not the main work of the system monitor, but they are also necessary.
func sysmon() {
...
for {
...
// flush heap profile
if shouldFlushHeapProfile() {
flushHeapProfile()
}
// drop mcache buffers
if shouldDropMcache() {
dropMcache()
}
...
}
}Overall, the system monitor is an important part of Go runtime. It monitors Go program runtime state and makes corresponding processing to ensure Go program stable operation.
Complete Example
The program below runs as-is: copy it into a file and execute go run main.go, or paste it into the Go Playground.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
stop := make(chan struct{})
// A busy goroutine that yields only when stop is closed
go func() {
for {
select {
case <-stop:
return
default:
}
}
}()
// Async preemption keeps timers running even while a goroutine is busy
time.Sleep(50 * time.Millisecond)
close(stop)
fmt.Println("timer fired while a goroutine was busy")
fmt.Println("GOMAXPROCS >= 1:", runtime.GOMAXPROCS(0) >= 1)
}Expected output:
timer fired while a goroutine was busy
GOMAXPROCS >= 1: trueCommon Pitfalls
- sysmon is runtime-internal. You cannot call it directly; understanding it helps you reason about pauses and preemption.
- Preemption became asynchronous in Go 1.14. Long CPU loops without function calls can now be interrupted via signals.
- Timers are not wall-clock precise. sysmon backs off to about 10ms when the program is idle.
- Blocking syscalls are monitored. A thread stuck in a syscall longer than ~20µs can trigger another P to run.
- Forced GC every 2 minutes of idle time. A mostly-idle server may still show periodic GC cycles caused by sysmon.
Change Log
- 2026-10-14: Added "Before You Start" notes, a complete runnable example with expected output, common pitfalls, and this change log.
