Long-running daemon deployment guide (v1.4.103+)¶
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.
v1.4.103 long-running hardening switches (env opt-in, default OFF)¶
v1.4.103 adds 2 env opt-in switches that 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.
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.
FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 — 1h-ahead proactive client_sig refresh¶
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:
- Start daemon:
FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 RUST_LOG=info futu-opend ... - log should contain "v1.4.103 G1: client_sig invalidate scheduled" + ttl_secs
- log should contain "v1.4.103 G1: spawning client_sig proactive timer"
- After timer fires (production typically 24-48h later), log should contain "v1.4.103 G1: refresh completed"
- After timer fires, daemon continues to query / push without disconnect = backend accepts ahead-of-time refresh.
FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 — reactive refresh on persistent tcp_login failures¶
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:
- In test environment, advance system clock by +30 days (past natural
client_sigexpiry), or wait 30 days of long-running. - Start daemon:
FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 ... - heartbeat must fail → reconnect monitor starts
- log should show "reconnect login failed" (×3)
- log should show "v1.4.103 G4: persistent tcp_login failures → trying reactive client_sig refresh"
- log should show "v1.4.103 G4: refresh_qot_login OK → reauth_via_remember_login"
- Subsequent
tcp_loginshould succeed; daemon recovers push.
Recommended deployment combinations¶
⚠️ 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.
Containerization examples¶
Docker¶
# docker-compose.yml fragment
services:
futu-opend:
image: ghcr.io/futuleaf/futu-opend-rs:v1.4.103
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 | v1.4.103 behavior | v1.4.103 behavior | env required |
|---|---|---|---|
client_sig natural expiry (30 days) |
daemon error → admin reload needed | G1 timer auto-renews 1h before | FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 |
| 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 | FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 |
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 | v1.4.103 G2 RepullAuthCode auto-refresh | same | (default ON since v1.4.103) |
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.
- "env FUTU_CLIENT_SIG_PROACTIVE_REFRESH not set to 1": env var didn't propagate.
- "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_sigfor C++-equivalent behavior.
For deployment issues, file an issue or contact via the official site.