cgo and Scheduler Transitions (incl. Go 1.26)

Updated

September 8, 2026

cgo and Scheduler Transitions (incl. Go 1.26)

Overview

cgo lets Go call C (and reverse), but each call crosses ABI, stack, and scheduler boundaries. High-frequency cgo is a classic latency and throughput tax.

Go 1.26 runtime work simplified how processors track syscall/cgo blocking: P no longer uses a dedicated syscall state; the scheduler derives blocking from G state. That reduces bookkeeping on hot syscall/cgo paths (community writeups cite meaningful wins especially for cgo-heavy workloads).

Diagram: cgo call path

  Go code
     │
     v
  runtime.cgocall
     │
     v
  system stack → C function (may block M)
     │
     v
  return to Go → reacquire P if needed

Call Path (Conceptual)

Go code
  -> runtime.cgocall
  -> switch to system stack
  -> C function runs (may block OS thread)
  -> return to Go
  -> reacquire P if needed

While C runs, that M is busy in C. The runtime may reassign P so other Go code continues — but threads can accumulate if many Ms stick in C.

Costs

  1. Call overhead (argument copy, stack switch)
  2. Loss of some Go compiler visibility
  3. Harder cross-compilation
  4. GC interactions with C-allocated memory (you free C memory)

Rules

Prefer Avoid
Pure Go libraries cgo in per-request tiny helpers
Batch work into fewer cgo calls Chatty call loops
Explicit C resource Close Relying only on finalizers
Build tags for optional cgo Silent cgo in libraries users don’t expect
CGO_ENABLED=0 go build ./...   # fail if cgo required
go env CGO_ENABLED

Debugging

# see if binary links libc unexpectedly
ldd ./app   # Linux

# profiles often show runtime.cgocall
go tool pprof cpu.out

Relation To Non-cgo Syscalls

Blocking syscalls from Go also park/reassign; netpoll avoids blocking Ms for sockets. cgo is closer to “unknown blocking foreign code.”

Experiment

# pure Go path baseline
CGO_ENABLED=0 go build -o /tmp/pure .

Write a tiny package without cgo and confirm it builds with CGO_ENABLED=0. If you have a cgo dependency, compare:

go test -bench=. -tags=cgo ./...

What to notice: Many services never need cgo; forcing CGO_ENABLED=0 in CI catches accidental deps early.

Try next: Read Go 1.26 release notes scheduler section alongside GMP scheduler. For the deployment cost of cgo (libc, Alpine, scratch), start at Go as a Systems Language.