Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Auto-Rotation

xrat supports automatic proxy rotation, periodically switching between configs based on a schedule, health checks, or manual triggers.

Overview

Auto-rotation is managed by the daemon supervisor. When enabled, the daemon:

  1. Periodically tests candidate configs
  2. Selects the best candidate based on latency
  3. Atomically disconnects the old session and connects the new one
  4. Respects cooldown periods to prevent rapid switching

Configuration

Enable and configure rotation in config.toml:

[runtime.rotation]
enabled = false
interval_secs = 1800
health_trigger_enabled = true
cooldown_secs = 300
test_concurrency = 0
test_stages = ["real_delay", "download"]
FieldDescriptionDefault
enabledEnable scheduled rotationfalse
interval_secsRotation interval in seconds1800 (30 minutes)
health_trigger_enabledTrigger rotation on health check failuretrue
cooldown_secsMinimum time between rotations300 (5 minutes)
test_concurrencyConcurrent test workers (0 = auto)0
test_stagesTest stages to run for candidate selection["real_delay", "download"]

Rotation Triggers

Timer Trigger

Scheduled rotation every interval_secs:

interval_secs = 1800  # rotate every 30 minutes

The daemon maintains a timer that fires at the specified interval, triggering rotation to the best available candidate.

Health Check Trigger

Triggered when the active proxy fails a health check:

health_trigger_enabled = true

The daemon monitors proxy health every 15 seconds. If the health check fails (process dead, port unreachable), rotation is triggered immediately.

Manual Trigger

Triggered by the user via CLI:

xrat proxy rotate

Manual rotation bypasses the timer but respects cooldown.

Forced Rotation

Rotate to a specific config:

xrat proxy rotate --config-id 99

Skips candidate selection and rotates to the specified config.

Cooldown Protection

After a rotation, the daemon enforces a cooldown period:

cooldown_secs = 300  # 5 minutes

During cooldown:

  • Timer triggers are delayed until cooldown expires
  • Health check triggers are suppressed (unless critical)
  • Manual triggers are allowed (user override)

Cooldown State

#![allow(unused)]
fn main() {
struct SupervisorState {
    cooldown_until: Option<DateTime<Utc>>,
    // ...
}
}

When a rotation completes:

#![allow(unused)]
fn main() {
state.cooldown_until = Some(Utc::now() + Duration::from_secs(cooldown_secs));
}

Candidate Selection

When rotating without --config-id, the daemon selects the best candidate:

Step 1: Load Candidates

Query enabled configs from the database:

SELECT * FROM configs
WHERE is_enabled = true
  AND is_deleted = false
  AND id != <current_config_id>

Step 2: Test Candidates

Run test stages on all candidates concurrently:

test_concurrency = 4  # test 4 configs at once
test_stages = ["real_delay", "download"]

For each candidate:

  1. Generate a probe config
  2. Spawn a short-lived Xray process
  3. Run the specified test stages
  4. Collect results (latency, throughput, success/failure)
  5. Terminate the probe process

Step 3: Filter Failures

Exclude configs that failed any test stage:

#![allow(unused)]
fn main() {
let successful = candidates.into_iter()
    .filter(|c| c.real_delay_ok && c.download_ok)
    .collect();
}

Step 4: Sort by Latency

Sort by real-delay latency (lowest first):

#![allow(unused)]
fn main() {
successful.sort_by_key(|c| c.real_delay_ms);
}

Step 5: Select Top Candidate

Pick the first config from the sorted list:

#![allow(unused)]
fn main() {
let best = successful.first()?;
}

If no candidates pass testing, rotation is skipped.

Rotation Flow

When rotation is triggered:

  1. Check cooldown β€” If active, delay or skip
  2. Select candidate β€” Run candidate selection (or use --config-id)
  3. Atomic replace β€” Disconnect old session, connect new session
  4. Update state β€” Record rotation timestamp, trigger type, new config ID
  5. Reset timer β€” Schedule next timer-based rotation

Atomic Replace

The replace flow ensures minimal downtime:

#![allow(unused)]
fn main() {
async fn replace_session(old_id: i64, new_config_id: i64) -> Result<()> {
    // 1. Start new session
    let new_session = connect(new_config_id).await?;

    // 2. Wait for new session to be ready
    wait_for_ready(new_session.socks_port).await?;

    // 3. Stop old session
    disconnect(old_id).await?;

    Ok(())
}
}

The new session is started before the old session is stopped, ensuring continuous connectivity.

Rotation Status

Check rotation status:

xrat proxy status

Output:

Proxy Rotation Status
─────────────────────────────────
Enabled:        yes
Interval:       1800s
Last rotation:  2026-05-28 10:30:00 (manual)
Next rotation:  2026-05-28 11:00:00
Cooldown:       300s (inactive)
Active config:  42 (vless://example.com:443)

JSON Output

xrat proxy status --json
{
  "enabled": true,
  "interval_secs": 1800,
  "last_trigger": "manual",
  "last_rotation_at": "2026-05-28T10:30:00Z",
  "next_rotation_at": "2026-05-28T11:00:00Z",
  "cooldown_secs": 300,
  "cooldown_active": false,
  "active_config_id": 42
}

Enabling Rotation

Start Rotation

xrat proxy start

Sends a ProxyStart request to the daemon, which enables the rotation scheduler.

Stop Rotation

xrat proxy stop

Sends a ProxyStop request to the daemon, which disables the rotation scheduler. The active proxy session continues running.

Rotation Strategies

Conservative Strategy

Long intervals, strict testing, long cooldown:

[runtime.rotation]
enabled = true
interval_secs = 3600  # 1 hour
health_trigger_enabled = true
cooldown_secs = 600   # 10 minutes
test_stages = ["real_delay", "download"]

Best for: stable connections, minimal disruption

Aggressive Strategy

Short intervals, fast testing, short cooldown:

[runtime.rotation]
enabled = true
interval_secs = 300   # 5 minutes
health_trigger_enabled = true
cooldown_secs = 60    # 1 minute
test_stages = ["real_delay"]

Best for: finding the fastest proxy, frequent optimization

Health-Only Strategy

No scheduled rotation, only rotate on failure:

[runtime.rotation]
enabled = true
interval_secs = 86400  # 24 hours (effectively disabled)
health_trigger_enabled = true
cooldown_secs = 300
test_stages = ["real_delay"]

Best for: stable connections with automatic failover

Persistence

Rotation state is tracked in memory (not persisted to database). On daemon restart:

  • Rotation is disabled (must be re-enabled with xrat proxy start)
  • Cooldown is reset
  • Timer is reset

The active session is persisted and reattached on daemon restart.

Troubleshooting

Rotation Not Triggering

Symptom: Timer fires but rotation doesn’t happen

Check:

  • Is cooldown active? xrat proxy status
  • Are there enabled configs? xrat list configs --enabled-only
  • Do candidates pass testing? xrat test --enabled-only

Rotation Fails

Symptom: Rotation triggers but new session fails to connect

Check:

  • Test the target config manually: xrat test <id>
  • Check daemon logs for errors
  • Verify Xray binary is available

Rapid Rotation

Symptom: Proxy switches too frequently

Fix: Increase cooldown:

cooldown_secs = 600  # 10 minutes

Or disable health trigger:

health_trigger_enabled = false