How Xray-core Stats Channels Enable Real-Time Traffic Monitoring

Xray-core provides a generic, thread-safe stats channel—implemented in app/stats/channel.go—that broadcasts live traffic metrics via a publish-subscribe model, allowing external tools to monitor bytes transferred, connection counts, and latency in real time without blocking the proxy data path.

The stats channel is the backbone of traffic observability in the XTLS/Xray-core proxy framework. By decoupling metric generation from consumption through a lock-protected Go channel, Xray-core ensures that high-throughput traffic processing remains unaffected while observers receive live statistic streams.

Architecture of the Stats Channel System

The stats subsystem centers on the stats.Channel interface defined in the stats manager (app/stats/stats.go). The following components collaborate to deliver real-time metrics:

  • Channel (app/stats/channel.go): Holds a buffered Go channel (chan channelMessage), a slice of subscriber channels, and synchronization primitives to broadcast messages safely.
  • ChannelConfig (app/stats/config.proto): Exposes tuning parameters including blocking mode, subscriber_limit, and buffer_size.
  • Manager (app/stats/stats.go): Registers, starts, and shuts down channels via RegisterChannel, GetChannel, Start, and Close.
  • Publishers: Components like inbound/outbound handlers that call Channel.Publish(ctx, msg) whenever metrics are generated.
  • Subscribers: External tools (CLI, gRPC clients, REST APIs) that obtain a receive-only channel via Channel.Subscribe() to consume messages as they arrive.

Channel Lifecycle

The lifecycle of a channel follows four distinct phases, each protected by mutexes to ensure thread safety under concurrent load.

1. Creation NewChannel(&ChannelConfig{...}) allocates the internal message channel with the configured buffer size and stores the limit and blocking flags【channel.go‑L33】.

2. Start c.Start() spawns a goroutine that continuously selects on:

  • Incoming channelMessage values (published by Publish) → broadcast to every subscriber (blocking or non-blocking depending on c.blocking)【channel.go‑L23, L99‑L124】.
  • The c.closed signal → gracefully closes all subscriber channels and exits【L15‑L21】.

3. Publish Publish(ctx, msg) wraps the payload in a channelMessage and either sends it synchronously (publish) or asynchronously (publishNonBlocking) based on the blocking flag【L71‑L83】. The context allows the publisher to discard a message if cancelled early【L38‑L49】.

4. Subscribe and Unsubscribe Subscribe() creates a new subscriber channel (respecting bufferSize and subsLimit) and appends it to the slice under a write lock【L44‑L53】. Unsubscribe() removes the channel safely under the same lock【L55‑L68】.

Real-Time Monitoring Data Flow

The stats channel facilitates live traffic monitoring through the following operational sequence:

  1. Configure: Users enable a channel in the Xray JSON/YAML configuration using parameters that mirror ChannelConfig (blocking mode, subscriber limit, buffer size).
  2. Manager Registration: When Xray-core boots, stats.NewManager registers a default channel via RegisterChannel and starts it if the manager is already running【stats.go‑L49】.
  3. Publisher Emission: Any component collecting metrics (bytes transferred, connection counts) calls manager.GetChannel("...").Publish(ctx, metricStruct). Because the channel runs its own goroutine, publishing never blocks the data path unless blocking mode is explicitly enabled.
  4. Subscriber Consumption: External tools (e.g., xray api stats CLI or gRPC clients) call Channel.Subscribe() to receive a <-chan interface{}. As soon as Publish occurs, the message forwards to every active subscriber, providing a live stream of traffic statistics.

The combination of a lock-protected subscriber list, optional non-blocking delivery, and context-aware cancellation ensures the monitoring pipeline remains lightweight and does not compromise proxy performance.

Practical Implementation Example

Below is a minimal Go example demonstrating how to create a manager, register a channel, publish a custom metric, and subscribe to the live stream.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/xtls/xray-core/app/stats"
)

type MyMetric struct {
	Tag   string
	Bytes int64
}

func main() {
	// 1️⃣ Initialise the stats manager
	mgr, _ := stats.NewManager(context.Background(), &stats.Config{})
	_ = mgr.Start() // Starts any pre-registered channels

	// 2️⃣ Register a custom channel called "traffic"
	ch, _ := mgr.RegisterChannel("traffic")

	// 3️⃣ Subscribe to the channel (e.g., a monitoring goroutine)
	sub, _ := ch.Subscribe()
	go func() {
		for msg := range sub {
			if m, ok := msg.(MyMetric); ok {
				fmt.Printf("[monitor] %s transferred %d bytes\n", m.Tag, m.Bytes)
			}
		}
	}()

	// 4️⃣ Simulate a publisher that emits metrics every second
	ctx := context.Background()
	go func() {
		for i := 0; i < 5; i++ {
			ch.Publish(ctx, MyMetric{Tag: "example.com", Bytes: int64(1024 * (i + 1))})
			time.Sleep(time.Second)
		}
	}()

	// Let the demo run briefly
	time.Sleep(6 * time.Second)
	_ = ch.Close()
}

Key implementation details demonstrated above:

  • Channel creation uses RegisterChannel, which internally calls NewChannel with a default buffer of 64 and non-blocking mode.
  • Publishing is a one-liner: ch.Publish(ctx, metric). The channelMessage wrapper ensures early-cancellation safety.
  • Subscription returns a chan interface{} that can be read in a separate goroutine, providing real-time updates as soon as Publish is called.

Configuring Stats Channels in Xray-core

Users enable and tune channels via the main configuration file. The following YAML example shows how to configure a non-blocking channel with custom buffer sizes:

stats:
  enabled: true
  channels:
    traffic:
      blocking: false        # non-blocking delivery (default)

      subscriber_limit: 0    # 0 = unlimited subscribers

      buffer_size: 128       # size of each subscriber's buffer

The blocking field determines whether publishers wait for the channel goroutine to accept the message (blocking) or drop messages when buffers are full (non-blocking). The subscriber_limit prevents memory exhaustion from uncontrolled subscriber growth.

Key Source Files in Xray-core

Understanding these files is essential for extending or debugging the stats system:

  • app/stats/channel.go: Core implementation of the publish-subscribe channel, including concurrency handling and optional blocking modes.
  • app/stats/config.proto: Protobuf definition for channel tuning parameters (Blocking, SubscriberLimit, BufferSize).
  • app/stats/stats.go: Manager that registers, starts, and shuts down channels; bridges the channel with the rest of Xray’s stats framework.
  • app/stats/command/command.proto: gRPC definition used by external tools (e.g., xray api stats) to subscribe to the live stats channel.
  • app/stats/channel_test.go: Unit tests demonstrating basic subscription and publishing workflows.

Summary

  • Xray-core stats channels provide a thread-safe, decoupled mechanism for real-time traffic monitoring using a publish-subscribe pattern.
  • The stats.Channel implementation in app/stats/channel.go manages subscriber lists with mutex protection and supports both blocking and non-blocking delivery modes.
  • Configuration via ChannelConfig allows tuning of buffer sizes and subscriber limits to balance observability with memory constraints.
  • External integration is available through gRPC commands defined in app/stats/command/command.proto, enabling tools like xray api to consume live metric streams.
  • The architecture ensures that metric collection never blocks the core proxy data path, maintaining high-throughput performance under load.

Frequently Asked Questions

What is the difference between blocking and non-blocking mode in Xray-core stats channels?

In blocking mode, the Publish call waits synchronously until the channel goroutine accepts the message, ensuring zero message loss at the cost of potential latency in the publisher. In non-blocking mode (the default), Publish returns immediately; if the internal buffer is full, the message is dropped. This prevents the proxy data path from stalling when monitoring consumers are slow, as implemented in channel.go【L71‑L83】.

How do external tools subscribe to Xray-core traffic statistics?

External tools call the Subscribe() method on a channel obtained via the stats manager, which returns a receive-only Go channel (<-chan interface{}). Real-world implementations typically use the gRPC interface defined in app/stats/command/command.proto, where the xray api stats CLI acts as a subscriber that receives serialized metric messages from the running Xray-core instance.

Can I configure multiple independent stats channels for different metric types?

Yes. The stats manager in app/stats/stats.go supports registering multiple named channels via RegisterChannel(name). You can configure separate channels for distinct concerns (e.g., "traffic_bytes", "connection_latency", "reject_counts") each with independent buffer sizes and subscriber limits, allowing fine-grained control over resource allocation per metric stream.

What happens to subscribers when a channel is closed?

When Close() is invoked on a channel, the internal c.closed signal triggers the broadcast goroutine to exit gracefully【channel.go‑L21】. All active subscriber channels are then closed, causing any goroutines ranging over <-chan interface{} to receive the zero value and exit their loops. This ensures no goroutine leaks occur during shutdown or configuration reloads.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →