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

> Discover how Xray-core stats channels enable real-time traffic monitoring. Gain insights into bytes, connections, and latency without impacting proxy performance.

- Repository: [Project X Community, Not Porn-jet X Hub/Xray-core](https://github.com/XTLS/Xray-core)
- Tags: deep-dive
- Published: 2026-04-21

---

**Xray-core provides a generic, thread-safe stats channel—implemented in [`app/stats/channel.go`](https://github.com/XTLS/Xray-core/blob/main/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](https://github.com/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`](https://github.com/XTLS/Xray-core/blob/main/app/stats/stats.go)). The following components collaborate to deliver real-time metrics:

- **Channel** ([`app/stats/channel.go`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/channel.go#L27)‑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`](https://github.com/XTLS/Xray-core/blob/main/channel.go#L5)‑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`](https://github.com/XTLS/Xray-core/blob/main/stats.go#L35)‑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.

```go
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:

```yaml
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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/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`](https://github.com/XTLS/Xray-core/blob/main/channel.go#L15)‑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.