Skip to content

Long-running daemon deployment guide

This guide covers configuration, self-healing, and fallback paths specific to running a daemon long-term (24h+ / 7 days / 30 days) in production.

Applicable scenarios: - VPS / cloud server running daemon 24×7 - Containerized deployment (Docker / Kubernetes) - Multi-account in a single daemon (multi-session) - Options / futures strategies needing persistent subscription + push

If you only run daemon locally for short stretches (CLI usage / manual testing), this guide is not required — defaults are sufficient.


Long-running hardening switches (config / env opt-in, default OFF)

These 2 opt-in switches let a long-running daemon proactively renew client_sig before invalidation, and proactively re-login on persistent failures, reducing the frequency of admin reload / restart needs. They were introduced in v1.4.103 and can now be enabled by CLI flags, TOML/XML config, or the compatibility env variables. The log examples below keep the historical tags.

Default OFF because both paths depend on server-side response behavior. Production deployments wanting to enable them should first validate the flow on a non-critical account by following this section.

--client-sig-proactive-refresh — 1h-ahead proactive client_sig refresh

./futu-opend --client-sig-proactive-refresh ...

TOML:

client_sig_proactive_refresh = true

Compatibility env:

FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 ./futu-opend ...

When effective: at startup, daemon reads the server-provided client_sig_invalid_local_time_s field from the /authority/ response. If this timestamp > 0 (server provided an expiry), the daemon spawns a timer that fires at (invalid_time - 3600s) to refresh qot-login, without waiting for backend errors.

Why default OFF: server acceptance of multi-hour-ahead refresh vs "too early" reject can depend on account and server state. Validate with:

  1. Start daemon: RUST_LOG=info futu-opend --client-sig-proactive-refresh ...
  2. log should contain "v1.4.103 G1: client_sig invalidate scheduled" + ttl_secs
  3. log should contain "v1.4.103 G1: spawning client_sig proactive timer"
  4. After timer fires (production typically 24-48h later), log should contain "v1.4.103 G1: refresh completed"
  5. After timer fires, daemon continues to query / push without disconnect = backend accepts ahead-of-time refresh.

--client-sig-reactive-refresh — reactive refresh on persistent tcp_login failures

./futu-opend --client-sig-reactive-refresh ...

TOML:

client_sig_reactive_refresh = true

Compatibility env:

FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 ./futu-opend ...

When effective: daemon TCP heartbeat fails → reconnect monitor starts. If tcp_login fails ≥ 3 consecutive times during reconnect, daemon assumes client_sig may be invalid, automatically calls AuthRefresher::refresh_qot_login() + auth::reauth_via_remember_login() to obtain a fresh AuthResult, swaps the cached client_sig, and the next tcp_login uses the fresh sig.

Why default OFF: tcp_login failure ret_type semantics are not exclusively "client_sig invalid" — they can also indicate server-side rate limiting or risk control. A misfired refresh may amplify limits. Validate with:

  1. In test environment, advance system clock by +30 days (past natural client_sig expiry), or wait 30 days of long-running.
  2. Start daemon: futu-opend --client-sig-reactive-refresh ...
  3. heartbeat must fail → reconnect monitor starts
  4. log should show "reconnect login failed" (×3)
  5. log should show "v1.4.103 G4: persistent tcp_login failures → trying reactive client_sig refresh"
  6. log should show "v1.4.103 G4: refresh_qot_login OK → reauth_via_remember_login"
  7. Subsequent tcp_login should succeed; daemon recovers push.

⚠️ G1 / G4 remain experimental (default OFF). Multi-hour-ahead refresh acceptance and cipher rate-limit thresholds can vary by account and server state. The table below is for self-validated deployments, not default production guidance.

Environment G1 G4 Notes
Local short-running (CLI / test) OFF OFF Defaults sufficient, no long-runner need
Production long-running (24h+) early adopter, self-validating ON (experimental) OFF Validate G1 on a non-critical account first; G4 still relies on admin reload
Production long-running (7 days+) full opt-in (experimental) ON (experimental) ON (experimental) Expect zero admin reload; validate on a non-critical account before using on critical accounts
Multi-account single daemon (5+ accounts) ON (experimental) OFF G4 risk: one account triggers rate-limit, others affected

⚠️ Validate the full long-running flow in your own environment before flipping these switches ON. Daemon logs redact sensitive values by default, but review logs before sharing. The current state does not claim these switches are safe as default production settings.

How to confirm current switches and ticket state

futucli daemon-status --rest-port <port>
futucli doctor --rest-port <port> --bundle ./doctor-bundle

daemon-status exposes the active client_sig refresh paths under login.auth_refresh. login.credential shows cached tgtgt saved_at, age_days, expires_in_days, and expiry_warning. When the ticket enters the 5-day window or is already expired, doctor emits an auth WARN; run futucli daemon-reload first.


Containerization examples

Docker

# docker-compose.yml fragment
services:
  futu-opend:
    image: ghcr.io/futuleaf/futu-opend-rs:v1.4.122
    restart: unless-stopped
    environment:
      FUTU_ACCOUNT: <account>
      FUTU_PWD: <password>
      FUTU_CLIENT_SIG_PROACTIVE_REFRESH: "1"   # long-runner hardening
      # FUTU_CLIENT_SIG_REACTIVE_REFRESH: "1"  # single-account scenarios only
    volumes:
      - ./.futu-opend-rs:/root/.futu-opend-rs    # device_id + credentials persistence
    ports:
      - "11111:11111"   # gRPC
      - "11112:11112"   # REST
      - "11113:11113"   # WebSocket

systemd unit

[Unit]
Description=futu-opend long-running gateway
After=network-online.target

[Service]
Type=simple
User=futu
EnvironmentFile=/etc/futu-opend.env
Environment=FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1
ExecStart=/usr/local/bin/futu-opend --rest-port 11112 --grpc-port 11111
Restart=on-failure
RestartSec=30s

[Install]
WantedBy=multi-user.target

/etc/futu-opend.env contains FUTU_ACCOUNT=... + FUTU_PWD=... (mode 0600, owner=futu).


Self-healing vs manual intervention

Scenario Default behavior With opt-in Enable with
client_sig natural expiry (30 days) daemon error → admin reload needed G1 timer auto-renews 1h before --client-sig-proactive-refresh or client_sig_proactive_refresh = true
Network jitter, tcp_login 1-2 fails exponential backoff retry same
Network jitter, tcp_login ≥ 3 fails persistent fail / manual intervention G4 auto-refresh + retry --client-sig-reactive-refresh or client_sig_reactive_refresh = true
Server rate-limit (error_code=15) 60s wait + retry same (G4 doesn't trigger on rate-limit)
Account risk control / password change / device lock Manual SMS / --reset-device same
broker auth_code 30-day expiry RepullAuthCode auto-refresh same default ON

Troubleshooting

G1 timer not spawning after enable

In log, look for "v1.4.103 G1 skip" reason: - "client_sig_invalid_local_time_s=0": backend didn't return this field → old backend / old credentials shell. --reset-device to redo password auth and obtain new field. - "client_sig_proactive_refresh disabled": proactive refresh was not enabled through CLI, TOML/XML, or env. - "TooLate": invalid_time already past or < 1h away, skipped. Restart daemon to let first login obtain new invalid_time.

G4 refresh not triggering

In log, look for "v1.4.103 G4 skip" reason: - "consecutive failures below threshold": failure count not yet 3 - "auth_refresher not injected": internal wiring issue → file an issue - "already attempted refresh this cycle": refresh-fail-loop guard. Restart daemon to reset.

G4 refresh failed, daemon continues with stale auth

By design. Log should contain "v1.4.103 G4: refresh_qot_login failed (continue with stale AuthResult)" or "v1.4.103 G4: reauth_via_remember_login failed (continue with stale AuthResult; user may need admin reload)". G4 is best-effort — failure doesn't break the existing fallback (admin reload path fully retained).


Verification Roadmap

  • After enough real deployments show G1/G4 do not trigger extra rate limiting or risk-control behavior, decide separately whether to flip default ON and update this guide's "default OFF" wording.
  • Actual moomoo-path broker channel routing requires separate validation; current builds only lay infrastructure, while routing still uses the main client_sig for C++-equivalent behavior.

For deployment issues, file an issue or contact via the official site.