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¶
TOML:
Compatibility env:
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:
RUST_LOG=info futu-opend --client-sig-proactive-refresh ... - 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.
--client-sig-reactive-refresh — reactive refresh on persistent tcp_login failures¶
TOML:
Compatibility env:
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-opend --client-sig-reactive-refresh ... - 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.
How to confirm current switches and ticket state¶
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_sigfor C++-equivalent behavior.
For deployment issues, file an issue or contact via the official site.