Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pion/ice/llms.txt

Use this file to discover all available pages before exploring further.

Pion ICE provides detailed statistics for monitoring connection quality, analyzing candidate pair performance, and debugging network issues. Statistics are accessible through thread-safe APIs that return snapshots of current metrics.

Candidate Pair Statistics

The primary statistics interface provides detailed metrics for candidate pairs:
stats.go
type CandidatePairStats struct {
    // Timestamp is the timestamp associated with this object.
    Timestamp time.Time

    // LocalCandidateID is the ID of the local candidate
    LocalCandidateID string

    // RemoteCandidateID is the ID of the remote candidate
    RemoteCandidateID string

    // State represents the state of the checklist for the local and remote
    // candidates in a pair.
    State CandidatePairState

    // Nominated is true when this valid pair that should be used for media
    // if it is the highest-priority one amongst those whose nominated flag is set
    Nominated bool

    // PacketsSent represents the total number of packets sent on this candidate pair.
    PacketsSent uint32

    // PacketsReceived represents the total number of packets received on this candidate pair.
    PacketsReceived uint32

    // BytesSent represents the total number of payload bytes sent on this candidate pair
    // not including headers or padding.
    BytesSent uint64

    // BytesReceived represents the total number of payload bytes received on this candidate pair
    // not including headers or padding.
    BytesReceived uint64

    // LastPacketSentTimestamp represents the timestamp at which the last packet was
    // sent on this particular candidate pair, excluding STUN packets.
    LastPacketSentTimestamp time.Time

    // LastPacketReceivedTimestamp represents the timestamp at which the last packet
    // was received on this particular candidate pair, excluding STUN packets.
    LastPacketReceivedTimestamp time.Time

    // TotalRoundTripTime represents the sum of all round trip time measurements
    // in seconds since the beginning of the session, based on STUN connectivity
    // check responses.
    TotalRoundTripTime float64

    // CurrentRoundTripTime represents the latest round trip time measured in seconds,
    // computed from both STUN connectivity checks, including those that are sent
    // for consent verification.
    CurrentRoundTripTime float64

    // RequestsReceived represents the total number of connectivity check requests
    // received (including retransmissions).
    RequestsReceived uint64

    // RequestsSent represents the total number of connectivity check requests
    // sent (not including retransmissions).
    RequestsSent uint64

    // ResponsesReceived represents the total number of connectivity check responses received.
    ResponsesReceived uint64

    // ResponsesSent represents the total number of connectivity check responses sent.
    ResponsesSent uint64
}

Getting Statistics

All Candidate Pairs

Retrieve statistics for all candidate pairs in the checklist:
stats := agent.GetCandidatePairsStats()

for _, pair := range stats {
    fmt.Printf("Pair: %s <-> %s\n", pair.LocalCandidateID, pair.RemoteCandidateID)
    fmt.Printf("  State: %s\n", pair.State)
    fmt.Printf("  Nominated: %v\n", pair.Nominated)
    fmt.Printf("  RTT: %.3fs\n", pair.CurrentRoundTripTime)
    fmt.Printf("  Bytes sent: %d, received: %d\n", pair.BytesSent, pair.BytesReceived)
    fmt.Printf("  Packets sent: %d, received: %d\n", pair.PacketsSent, pair.PacketsReceived)
}

Selected Candidate Pair

Get statistics for only the currently selected (nominated) pair:
stats, ok := agent.GetSelectedCandidatePairStats()
if !ok {
    fmt.Println("No selected candidate pair yet")
    return
}

fmt.Printf("Selected pair RTT: %.3fs\n", stats.CurrentRoundTripTime)
fmt.Printf("Total data sent: %d bytes\n", stats.BytesSent)
fmt.Printf("Total data received: %d bytes\n", stats.BytesReceived)
GetSelectedCandidatePairStats returns false if no pair has been nominated yet. Check the boolean return value before accessing statistics.

Candidate Statistics

Get information about individual local or remote candidates:
stats.go
type CandidateStats struct {
    // Timestamp is the timestamp associated with this object.
    Timestamp time.Time

    // ID is the candidate ID
    ID string

    // NetworkType represents the type of network interface used by the base of a
    // local candidate.
    NetworkType NetworkType

    // IP is the IP address of the candidate
    IP string

    // Port is the port number of the candidate.
    Port int

    // CandidateType is the "Type" field of the ICECandidate.
    CandidateType CandidateType

    // Priority is the "Priority" field of the ICECandidate.
    Priority uint32

    // URL is the URL of the TURN or STUN server indicated in the that translated
    // this IP address.
    URL string

    // RelayProtocol is the protocol used by the endpoint to communicate with the
    // TURN server. Valid values: UDP, TCP, or TLS.
    RelayProtocol string

    // Deleted is true if the candidate has been deleted/freed.
    Deleted bool
}

Local Candidates

localStats := agent.GetLocalCandidatesStats()

for _, cand := range localStats {
    fmt.Printf("Local: %s %s:%d (type=%s, priority=%d)\n",
        cand.NetworkType, cand.IP, cand.Port, cand.CandidateType, cand.Priority)
    
    if cand.CandidateType == ice.CandidateTypeRelay {
        fmt.Printf("  TURN server: %s (protocol: %s)\n", cand.URL, cand.RelayProtocol)
    }
}

Remote Candidates

remoteStats := agent.GetRemoteCandidatesStats()

for _, cand := range remoteStats {
    fmt.Printf("Remote: %s %s:%d (type=%s)\n",
        cand.NetworkType, cand.IP, cand.Port, cand.CandidateType)
}

Monitoring Connection Quality

Real-Time RTT Monitoring

package main

import (
    "fmt"
    "time"
    
    "github.com/pion/ice/v4"
)

func monitorConnectionQuality(agent *ice.Agent) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        stats, ok := agent.GetSelectedCandidatePairStats()
        if !ok {
            continue
        }
        
        // Monitor RTT
        rtt := stats.CurrentRoundTripTime
        if rtt > 0.3 {
            fmt.Printf("WARNING: High RTT detected: %.3fs\n", rtt)
        }
        
        // Monitor packet loss
        totalSent := stats.PacketsSent
        totalReceived := stats.PacketsReceived
        if totalSent > 0 {
            lossRate := float64(totalSent-totalReceived) / float64(totalSent) * 100
            if lossRate > 5.0 {
                fmt.Printf("WARNING: Packet loss: %.2f%%\n", lossRate)
            }
        }
        
        // Monitor data rates
        timeSinceStart := time.Since(stats.Timestamp).Seconds()
        if timeSinceStart > 0 {
            sendRate := float64(stats.BytesSent) / timeSinceStart / 1024 / 1024 // MB/s
            recvRate := float64(stats.BytesReceived) / timeSinceStart / 1024 / 1024
            fmt.Printf("Rates - Send: %.2f MB/s, Recv: %.2f MB/s\n", sendRate, recvRate)
        }
    }
}

Bandwidth Estimation

type BandwidthEstimator struct {
    lastStats     ice.CandidatePairStats
    lastCheckTime time.Time
}

func (b *BandwidthEstimator) Update(stats ice.CandidatePairStats) (sendBW, recvBW float64) {
    if b.lastCheckTime.IsZero() {
        b.lastStats = stats
        b.lastCheckTime = time.Now()
        return 0, 0
    }
    
    elapsed := time.Since(b.lastCheckTime).Seconds()
    if elapsed < 1.0 {
        return 0, 0 // Need at least 1 second interval
    }
    
    // Calculate bytes transferred since last check
    bytesSent := stats.BytesSent - b.lastStats.BytesSent
    bytesRecv := stats.BytesReceived - b.lastStats.BytesReceived
    
    // Calculate bandwidth in bits per second
    sendBW = float64(bytesSent) * 8 / elapsed
    recvBW = float64(bytesRecv) * 8 / elapsed
    
    // Update for next calculation
    b.lastStats = stats
    b.lastCheckTime = time.Now()
    
    return sendBW, recvBW
}

// Usage
estimator := &BandwidthEstimator{}
ticker := time.NewTicker(2 * time.Second)
for range ticker.C {
    stats, ok := agent.GetSelectedCandidatePairStats()
    if !ok {
        continue
    }
    
    sendBW, recvBW := estimator.Update(stats)
    if sendBW > 0 {
        fmt.Printf("Bandwidth - Send: %.2f kbps, Recv: %.2f kbps\n",
            sendBW/1000, recvBW/1000)
    }
}

Average RTT Calculation

Calculate average RTT from total measurements:
stats, ok := agent.GetSelectedCandidatePairStats()
if !ok || stats.ResponsesReceived == 0 {
    return
}

averageRTT := stats.TotalRoundTripTime / float64(stats.ResponsesReceived)
fmt.Printf("Average RTT: %.3fs over %d measurements\n",
    averageRTT, stats.ResponsesReceived)

Connection Activity Tracking

Detect idle connections or stalled traffic:
func checkConnectionActivity(agent *ice.Agent, timeout time.Duration) bool {
    stats, ok := agent.GetSelectedCandidatePairStats()
    if !ok {
        return false
    }
    
    // Check when we last received data
    lastActivity := stats.LastPacketReceivedTimestamp
    if lastActivity.IsZero() {
        lastActivity = stats.LastPacketSentTimestamp
    }
    
    if time.Since(lastActivity) > timeout {
        fmt.Println("Connection appears idle")
        return false
    }
    
    return true
}

// Usage
if !checkConnectionActivity(agent, 30*time.Second) {
    // Connection has been idle for 30+ seconds
    // Consider reconnecting or alerting
}

Diagnostic Report

Generate a comprehensive diagnostic report:
func generateDiagnosticReport(agent *ice.Agent) {
    fmt.Println("=== ICE Connection Diagnostic Report ===")
    
    // Selected pair
    selectedStats, ok := agent.GetSelectedCandidatePairStats()
    if ok {
        fmt.Println("\nSelected Candidate Pair:")
        fmt.Printf("  State: %s (Nominated: %v)\n", selectedStats.State, selectedStats.Nominated)
        fmt.Printf("  Current RTT: %.3fs\n", selectedStats.CurrentRoundTripTime)
        fmt.Printf("  Average RTT: %.3fs\n", 
            selectedStats.TotalRoundTripTime/float64(selectedStats.ResponsesReceived))
        fmt.Printf("  Packets: %d sent, %d received\n", 
            selectedStats.PacketsSent, selectedStats.PacketsReceived)
        fmt.Printf("  Bytes: %d sent, %d received\n",
            selectedStats.BytesSent, selectedStats.BytesReceived)
        fmt.Printf("  STUN: %d requests sent, %d responses received\n",
            selectedStats.RequestsSent, selectedStats.ResponsesReceived)
    }
    
    // All pairs
    allPairs := agent.GetCandidatePairsStats()
    fmt.Printf("\nTotal Candidate Pairs: %d\n", len(allPairs))
    
    stateCount := make(map[ice.CandidatePairState]int)
    for _, pair := range allPairs {
        stateCount[pair.State]++
    }
    
    fmt.Println("Pair States:")
    for state, count := range stateCount {
        fmt.Printf("  %s: %d\n", state, count)
    }
    
    // Local candidates
    localCands := agent.GetLocalCandidatesStats()
    fmt.Printf("\nLocal Candidates: %d\n", len(localCands))
    typeCount := make(map[ice.CandidateType]int)
    for _, cand := range localCands {
        typeCount[cand.CandidateType]++
    }
    for candType, count := range typeCount {
        fmt.Printf("  %s: %d\n", candType, count)
    }
    
    // Remote candidates
    remoteCands := agent.GetRemoteCandidatesStats()
    fmt.Printf("\nRemote Candidates: %d\n", len(remoteCands))
}

Metrics Export

Export statistics in structured format for monitoring systems:
import "encoding/json"

type MetricsSnapshot struct {
    Timestamp      time.Time                    `json:"timestamp"`
    SelectedPair   *ice.CandidatePairStats      `json:"selected_pair"`
    AllPairs       []ice.CandidatePairStats     `json:"all_pairs"`
    LocalCandidates []ice.CandidateStats        `json:"local_candidates"`
    RemoteCandidates []ice.CandidateStats       `json:"remote_candidates"`
}

func exportMetrics(agent *ice.Agent) ([]byte, error) {
    snapshot := MetricsSnapshot{
        Timestamp:        time.Now(),
        AllPairs:         agent.GetCandidatePairsStats(),
        LocalCandidates:  agent.GetLocalCandidatesStats(),
        RemoteCandidates: agent.GetRemoteCandidatesStats(),
    }
    
    if stats, ok := agent.GetSelectedCandidatePairStats(); ok {
        snapshot.SelectedPair = &stats
    }
    
    return json.MarshalIndent(snapshot, "", "  ")
}

// Usage
metrics, err := exportMetrics(agent)
if err != nil {
    return err
}
fmt.Println(string(metrics))

STUN Request/Response Analysis

Monitor connectivity check patterns:
stats, ok := agent.GetSelectedCandidatePairStats()
if !ok {
    return
}

// Check if connectivity checks are working
if stats.RequestsSent > 0 && stats.ResponsesReceived == 0 {
    fmt.Println("WARNING: No STUN responses received")
    fmt.Println("Possible causes:")
    fmt.Println("  - Firewall blocking STUN traffic")
    fmt.Println("  - Remote peer not responding")
    fmt.Println("  - Network connectivity issues")
}

// Calculate response rate
if stats.RequestsSent > 0 {
    responseRate := float64(stats.ResponsesReceived) / float64(stats.RequestsSent) * 100
    fmt.Printf("STUN response rate: %.2f%%\n", responseRate)
    
    if responseRate < 80.0 {
        fmt.Println("WARNING: Low STUN response rate")
    }
}

Thread Safety

All statistics methods are thread-safe. They use the agent’s internal event loop to safely access state, so you can call them from any goroutine without additional synchronization.
agent_stats.go
func (a *Agent) GetCandidatePairsStats() []CandidatePairStats {
    var res []CandidatePairStats
    err := a.loop.Run(a.loop, func(_ context.Context) {
        // Safely access agent state within event loop
        result := make([]CandidatePairStats, 0, len(a.checklist))
        for _, cp := range a.checklist {
            // Collect statistics
        }
        res = result
    })
    // ...
}

Best Practices

Balance between visibility and performance:
  • Selected pair monitoring: Every 1-5 seconds for connection quality
  • All pairs: Every 10-30 seconds for detailed analysis
  • Candidates: Once per gathering phase or on demand
Avoid polling faster than 1 Hz unless absolutely necessary.
Statistics methods return copies of data, not references:
// Each call allocates new slice and structs
stats := agent.GetCandidatePairsStats()
For high-frequency monitoring, reuse buffers or limit scope:
// Only get what you need
stats, ok := agent.GetSelectedCandidatePairStats()
  • CurrentRoundTripTime: Latest measurement, subject to variance
  • TotalRoundTripTime / ResponsesReceived: Average, more stable
Use moving averages or percentiles for decision-making:
// Track last N measurements for percentile calculation
type RTTTracker struct {
    measurements []float64
    maxSize      int
}
Monitor for patterns indicating network issues:
  • Sudden RTT increase (>2x previous)
  • Zero packets received for >10 seconds
  • STUN response rate drops below 50%
  • Bytes sent/received stops incrementing
Trigger reconnection or renomination based on thresholds.

Reference

  • Candidate Pair Stats: stats.go:10 - CandidatePairStats type definition
  • Candidate Stats: stats.go:170 - CandidateStats type definition
  • Agent Methods: agent_stats.go:12 - GetCandidatePairsStats, GetSelectedCandidatePairStats
  • Pair Info: stats.go:143 - CandidatePairInfo for renomination