Hyperliquid: websocket watch tasks die with UnsubscribeError - no candles ever arrive over WS, REST fallback and heavy log spam
Environment
- freqtrade 2026.8 (
freqtradeorg/freqtrade:stableimage), ccxt 4.5.76 trading_mode: futures,margin_mode: isolated,dry_run: true- exchange
hyperliquid(USDC), Hyperliquid strategyNostalgiaForInfinityX7, 57-pair volume pairlist - public market data only (no
walletAddress/privateKeyneeded for a dry run)
What happens
The websocket never delivers a candle for any pair — for a ~10.5 h run:
- 0 occurrences of
watch donein the log (i.e. no WS data consumed at all) - ~3,000 errors with tracebacks:
ERROR - Exception in continuously_async_watch_ohlcv for XRP/USDC:USDC, 5m
Traceback (most recent call last):
...
File "ccxt/pro/hyperliquid.py", line ..., in watch_ohlcv
return await self.watch(url, messageHash, message, messageHash)
ccxt.base.errors.UnsubscribeError: hyperliquid candles:5m:XRP/USDC:USDC
- ~2,190
Task finished - donelines for the same run — watch tasks ending and being re-scheduled continuously Couldn't reuse watch ... falling back to REST apiandOutdated historywarningsRateLimitExceeded ... 429 Too Many Requestswarnings from the REST API
Effect: because all candle data falls back to REST, data ends up 10–20 minutes stale (5m data refreshing every 5–20 min instead of every 5), and the exchange's per-IP rate limit is hit.
Cause
This is a ccxt issue, reproduced outside of freqtrade: unsubscribing and re-subscribing the same pair/timeframe before the unsubscribe ack arrives makes the ack reject the newly armed subscription future.
- ccxt issue: https://github.com/ccxt/ccxt/issues/30419
- Standalone reproducer (no freqtrade involved, ~30 s):
1 control: fresh subscribe OK 1 candle(s)
2 control: re-subscribe (acked) OK 1 candle(s)
3 bug: re-subscribe (race) UnsubscribeError hyperliquid candles:5m:XRP/USDC:USDC
4 recovery: subscribe again OK 1 candle(s)
freqtrade produces the failing sequence routinely: cleanup_expired() drops a pair → the watch task
ends → _continuous_stopped fires _unwatch_ohlcv() → and the pair is re-scheduled (schedule_ohlcv)
shortly after. The ack for that unsubscribe then rejects the future of the new subscription, so the
new watch task dies with ccxt.UnsubscribeError.
The death is what makes it self-sustaining: _continuous_stopped fires another unwatch for the dead
task, whose ack rejects the next subscription, and so on — hence the thousands of errors and the
complete absence of WS data.
Why this is not #12739
#12739 shows the same Couldn't reuse watch lines but was resolved as a config problem
(privateKey vs privatekey) plus Outdated history on illiquid pairs. Here the websocket never
delivers for any pair (including BTC), the errors are continuous rather than per-pair, and the
failure reproduces without freqtrade in the loop (reproducer above).
Local workaround (not a proposed freqtrade fix)
We patched ccxt's un_watch_ohlcv() to a no-op for hyperliquid locally, so the ack that poisons the
next subscription never happens. Result after the change: 0 errors, 0 tracebacks, the WS delivers
(measured 114 consumptions per cycle on 57 pairs × 5 timeframes), no rate-limit warnings, live 5m
data. It is a workaround only — subscriptions are not released — so it is not proposed upstream.
Request
Harden the watch loop so a single UnsubscribeError cannot end the stream:
_continuously_async_watch_ohlcv() currently catches ccxt.BaseError, logs a traceback and
terminates the task; terminating triggers another unwatch, which feeds the loop described above.
After an UnsubscribeError, ccxt has already cleaned up its state (subscription marker deleted,
future rejected), so simply subscribing again succeeds — step 4 of the reproducer shows that, and
it is also what a retry in the task achieves.
I have a small patch + regression test for this (catch ccxt.UnsubscribeError inside the loop and
retry) and can open a PR if that is the preferred direction:
Source: freqtrade/freqtrade