Skip to main content

Overview

This guide covers performance best practices, caching strategies, and optimization techniques for gopsutil applications.

Understanding Performance Characteristics

Blocking Operations

Some gopsutil operations can block for extended periods:
import (
    "context"
    "time"
    "github.com/shirou/gopsutil/v4/cpu"
)

// ❌ BAD: This blocks for the full interval
func slowCPUCheck() {
    // Blocks for 1 second
    percent, _ := cpu.Percent(1*time.Second, false)
    fmt.Printf("CPU: %.2f%%\n", percent[0])
}

// ✅ GOOD: Use context with timeout for control
func fastCPUCheck() {
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()
    
    // Can be interrupted if context times out
    percent, err := cpu.PercentWithContext(ctx, 100*time.Millisecond, false)
    if err != nil {
        log.Printf("Failed to get CPU: %v", err)
        return
    }
    fmt.Printf("CPU: %.2f%%\n", percent[0])
}

Use Zero Interval for Cached Data

CPU percent can use cached values from the last call:
import (
    "github.com/shirou/gopsutil/v4/cpu"
)

// First call establishes baseline
func initCPUMonitoring() {
    // Initialize the internal cache
    _, _ = cpu.Percent(100*time.Millisecond, false)
}

// Subsequent calls use cached baseline
func getCurrentCPU() ([]float64, error) {
    // ✅ Uses cached previous value, returns immediately
    return cpu.Percent(0, false)
}

// In your monitoring loop:
func monitorCPU() {
    initCPUMonitoring()
    
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        // Fast - compares current vs cached
        percent, err := getCurrentCPU()
        if err != nil {
            log.Printf("Error: %v", err)
            continue
        }
        fmt.Printf("CPU: %.2f%%\n", percent[0])
    }
}

Caching Strategies

Implement Your Own Cache

Cache infrequently-changing data like CPU info:
import (
    "sync"
    "time"
    "github.com/shirou/gopsutil/v4/cpu"
)

type CPUInfoCache struct {
    mu         sync.RWMutex
    info       []cpu.InfoStat
    lastUpdate time.Time
    ttl        time.Duration
}

func NewCPUInfoCache(ttl time.Duration) *CPUInfoCache {
    return &CPUInfoCache{
        ttl: ttl,
    }
}

func (c *CPUInfoCache) Get(ctx context.Context) ([]cpu.InfoStat, error) {
    // Try read lock first (fast path)
    c.mu.RLock()
    if time.Since(c.lastUpdate) < c.ttl && c.info != nil {
        info := c.info
        c.mu.RUnlock()
        return info, nil
    }
    c.mu.RUnlock()
    
    // Cache miss or expired, acquire write lock
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // Double-check after acquiring write lock
    if time.Since(c.lastUpdate) < c.ttl && c.info != nil {
        return c.info, nil
    }
    
    // Fetch fresh data
    info, err := cpu.InfoWithContext(ctx)
    if err != nil {
        return nil, err
    }
    
    c.info = info
    c.lastUpdate = time.Now()
    return info, nil
}

// Usage
func main() {
    cache := NewCPUInfoCache(5 * time.Minute)
    
    // Multiple calls within 5 minutes use cached value
    info1, _ := cache.Get(context.Background())
    info2, _ := cache.Get(context.Background()) // From cache
    
    _ = info1
    _ = info2
}

Cache Multiple Metrics

Combine multiple metrics in a single cache:
type SystemMetrics struct {
    CPUPercent   []float64
    MemoryUsed   uint64
    MemoryTotal  uint64
    DiskUsed     uint64
    DiskTotal    uint64
    CollectedAt  time.Time
}

type MetricsCache struct {
    mu      sync.RWMutex
    metrics *SystemMetrics
    ttl     time.Duration
}

func (m *MetricsCache) Get(ctx context.Context) (*SystemMetrics, error) {
    m.mu.RLock()
    if m.metrics != nil && time.Since(m.metrics.CollectedAt) < m.ttl {
        cached := m.metrics
        m.mu.RUnlock()
        return cached, nil
    }
    m.mu.RUnlock()
    
    return m.refresh(ctx)
}

func (m *MetricsCache) refresh(ctx context.Context) (*SystemMetrics, error) {
    m.mu.Lock()
    defer m.mu.Unlock()
    
    // Collect all metrics in parallel
    type result struct {
        cpu  []float64
        mem  *mem.VirtualMemoryStat
        disk *disk.UsageStat
        err  error
    }
    
    ch := make(chan result, 1)
    
    go func() {
        var r result
        
        r.cpu, r.err = cpu.PercentWithContext(ctx, 0, false)
        if r.err != nil {
            ch <- r
            return
        }
        
        r.mem, r.err = mem.VirtualMemoryWithContext(ctx)
        if r.err != nil {
            ch <- r
            return
        }
        
        r.disk, r.err = disk.UsageWithContext(ctx, "/")
        ch <- r
    }()
    
    select {
    case r := <-ch:
        if r.err != nil {
            return nil, r.err
        }
        
        m.metrics = &SystemMetrics{
            CPUPercent:  r.cpu,
            MemoryUsed:  r.mem.Used,
            MemoryTotal: r.mem.Total,
            DiskUsed:    r.disk.Used,
            DiskTotal:   r.disk.Total,
            CollectedAt: time.Now(),
        }
        return m.metrics, nil
        
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}

Concurrent Collection

Collect Metrics in Parallel

import (
    "context"
    "sync"
    "github.com/shirou/gopsutil/v4/cpu"
    "github.com/shirou/gopsutil/v4/mem"
    "github.com/shirou/gopsutil/v4/disk"
)

type Metrics struct {
    CPU  []float64
    Mem  *mem.VirtualMemoryStat
    Disk *disk.UsageStat
}

func collectMetricsParallel(ctx context.Context) (*Metrics, error) {
    var wg sync.WaitGroup
    metrics := &Metrics{}
    errChan := make(chan error, 3)
    
    // CPU
    wg.Add(1)
    go func() {
        defer wg.Done()
        cpu, err := cpu.PercentWithContext(ctx, 0, false)
        if err != nil {
            errChan <- fmt.Errorf("cpu: %w", err)
            return
        }
        metrics.CPU = cpu
    }()
    
    // Memory
    wg.Add(1)
    go func() {
        defer wg.Done()
        mem, err := mem.VirtualMemoryWithContext(ctx)
        if err != nil {
            errChan <- fmt.Errorf("mem: %w", err)
            return
        }
        metrics.Mem = mem
    }()
    
    // Disk
    wg.Add(1)
    go func() {
        defer wg.Done()
        disk, err := disk.UsageWithContext(ctx, "/")
        if err != nil {
            errChan <- fmt.Errorf("disk: %w", err)
            return
        }
        metrics.Disk = disk
    }()
    
    wg.Wait()
    close(errChan)
    
    // Return first error if any
    if err := <-errChan; err != nil {
        return nil, err
    }
    
    return metrics, nil
}

Use Worker Pools for Process Monitoring

import (
    "context"
    "github.com/shirou/gopsutil/v4/process"
)

type ProcessMetrics struct {
    PID        int32
    Name       string
    CPUPercent float64
    MemoryMB   uint64
}

func monitorProcesses(ctx context.Context, pids []int32, workers int) ([]ProcessMetrics, error) {
    jobs := make(chan int32, len(pids))
    results := make(chan ProcessMetrics, len(pids))
    
    // Start workers
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for pid := range jobs {
                if metric, err := getProcessMetric(ctx, pid); err == nil {
                    results <- metric
                }
            }
        }()
    }
    
    // Send jobs
    for _, pid := range pids {
        jobs <- pid
    }
    close(jobs)
    
    // Wait and close results
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Collect results
    var metrics []ProcessMetrics
    for m := range results {
        metrics = append(metrics, m)
    }
    
    return metrics, nil
}

func getProcessMetric(ctx context.Context, pid int32) (ProcessMetrics, error) {
    p, err := process.NewProcessWithContext(ctx, pid)
    if err != nil {
        return ProcessMetrics{}, err
    }
    
    name, _ := p.NameWithContext(ctx)
    cpuPercent, _ := p.CPUPercentWithContext(ctx)
    memInfo, _ := p.MemoryInfoWithContext(ctx)
    
    return ProcessMetrics{
        PID:        pid,
        Name:       name,
        CPUPercent: cpuPercent,
        MemoryMB:   memInfo.RSS / 1024 / 1024,
    }, nil
}

Reduce Allocation Overhead

Reuse Slices

type Monitor struct {
    cpuBuf []float64 // Reusable buffer
}

func (m *Monitor) GetCPU(ctx context.Context) ([]float64, error) {
    percent, err := cpu.PercentWithContext(ctx, 0, true)
    if err != nil {
        return nil, err
    }
    
    // Reuse buffer if capacity is sufficient
    if cap(m.cpuBuf) >= len(percent) {
        m.cpuBuf = m.cpuBuf[:len(percent)]
        copy(m.cpuBuf, percent)
        return m.cpuBuf, nil
    }
    
    m.cpuBuf = make([]float64, len(percent))
    copy(m.cpuBuf, percent)
    return m.cpuBuf, nil
}

Pre-allocate Slices

func getAllProcesses(ctx context.Context) ([]*process.Process, error) {
    pids, err := process.PidsWithContext(ctx)
    if err != nil {
        return nil, err
    }
    
    // Pre-allocate with known capacity
    processes := make([]*process.Process, 0, len(pids))
    
    for _, pid := range pids {
        p, err := process.NewProcessWithContext(ctx, pid)
        if err != nil {
            continue
        }
        processes = append(processes, p)
    }
    
    return processes, nil
}

Optimize Polling Intervals

Adaptive Polling

type AdaptiveMonitor struct {
    interval    time.Duration
    minInterval time.Duration
    maxInterval time.Duration
}

func (m *AdaptiveMonitor) Monitor(ctx context.Context) {
    ticker := time.NewTicker(m.interval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            percent, err := cpu.PercentWithContext(ctx, 0, false)
            if err != nil {
                continue
            }
            
            // Adjust polling based on CPU usage
            if percent[0] > 80 {
                // High CPU - poll more frequently
                m.interval = m.minInterval
            } else if percent[0] < 20 {
                // Low CPU - poll less frequently
                m.interval = m.maxInterval
            }
            
            ticker.Reset(m.interval)
        }
    }
}

Batch Operations

Batch Process Queries

func getProcessesBatch(ctx context.Context, pids []int32, batchSize int) ([]ProcessMetrics, error) {
    var results []ProcessMetrics
    
    for i := 0; i < len(pids); i += batchSize {
        end := i + batchSize
        if end > len(pids) {
            end = len(pids)
        }
        
        batch := pids[i:end]
        metrics, err := monitorProcesses(ctx, batch, 4)
        if err != nil {
            return nil, err
        }
        
        results = append(results, metrics...)
        
        // Small delay between batches to avoid overwhelming system
        if end < len(pids) {
            time.Sleep(10 * time.Millisecond)
        }
    }
    
    return results, nil
}

Memory Profiling

Profile your application to identify bottlenecks:
import (
    "runtime/pprof"
    "os"
)

func profileMemory() {
    f, _ := os.Create("mem.prof")
    defer f.Close()
    
    // Your code here
    collectMetrics()
    
    pprof.WriteHeapProfile(f)
}

// Analyze with:
// go tool pprof mem.prof

Benchmarking

import "testing"

func BenchmarkCPUPercent(b *testing.B) {
    ctx := context.Background()
    
    // Initialize cache
    cpu.Percent(100*time.Millisecond, false)
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := cpu.PercentWithContext(ctx, 0, false)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkMemoryInfo(b *testing.B) {
    ctx := context.Background()
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := mem.VirtualMemoryWithContext(ctx)
        if err != nil {
            b.Fatal(err)
        }
    }
}

Performance Best Practices

1

Use context with timeouts

Always set reasonable timeouts to prevent hanging operations.
2

Cache static data

Cache CPU info and other rarely-changing data.
3

Use zero-interval CPU percent

After initialization, use Percent(0, false) for fast cached reads.
4

Collect metrics in parallel

Use goroutines to collect independent metrics concurrently.
5

Implement rate limiting

Avoid polling too frequently - balance freshness with resource usage.
6

Reuse buffers

Pre-allocate and reuse slices to reduce garbage collection.
7

Monitor in batches

When monitoring many processes, use batching and worker pools.
8

Profile and benchmark

Measure actual performance - don’t guess.

Performance Comparison

OperationTypical DurationOptimization
cpu.Percent(1s, false)~1 secondUse cpu.Percent(0, false)
cpu.Info()~5-10msCache for 5+ minutes
mem.VirtualMemory()~1-5msCache for 1+ seconds
process.NewProcess()<1msMinimal overhead
process.CPUPercent()~100msNeeds sampling interval
disk.Usage()~5-20msCache for 10+ seconds

Cross-Platform

Platform-specific performance characteristics

Error Handling

Handle errors efficiently

Build docs developers (and LLMs) love