Troubleshooting
A pragmatic fix-guide for the most common problems. For each symptom: brief explanation → diagnostics → fix.
General: reading logs
journalctl -u dvb-warppool -f --since "5 min ago"
For Docker:
docker logs -f dvb-warppool --since 5m
Temporarily raise the log level:
RUST_LOG=warppool=debug,info dvb-warppool-daemon
Targeted per crate:
RUST_LOG=warppool_stratum_v1=trace,warppool_bitcoin_rpc=debug,info dvb-warppool-daemon
RPC
getblockchaininfo failed: HTTP 401 — credentials rejected
Cookie or user/pass mismatch.
Diagnosis:
# Cookie auth:
cat ~/.bitcoin/.cookie # should be "user:hexhash"
# user/pass auth:
grep -E "^rpc(user|password)" ~/.bitcoin/bitcoin.conf
Fix:
- Cookie mode: make sure
node.rpc_cookie_pathinconfig.tomlpoints to the correct file. Bitcoin Core has its own cookie files for regtest/testnet (~/.bitcoin/regtest/.cookie,~/.bitcoin/testnet3/.cookie). The setup wizard typically shows the right path. - user/pass mode: in
secrets.toml,rpc_user+rpc_passmust match the values inbitcoin.conf. Insecrets.toml, notconfig.toml— otherwise the daemon complains at startup. - On permission errors: bitcoind runs as a different user; the cookie is
0600and unreadable for your warppool user. The setup wizard catches this and suggestschmod g+r+ group-add.
getblocktemplate failed: -8 ...
Bitcoin Core is reachable, but says no to the template.
Possible causes:
- IBD running:
warppool_rpc_ibd == 1. Wait until sync completes. - Wallet locked:
getblocktemplatewithout-walletbroadcast=0needs an unlocked wallet in Core. Unusual in a pool context, though — we use GBT without a wallet. - Too few peers:
getpeerinfoshows 0 or 1. Core then refuses to mine to avoid ending up on a minority fork. Fix: wait, or use-minimumchainwork=0for tests.
RPC latency high (Prometheus shows p99 > 1s)
Diagnosis:
- Are many RPCs running in parallel? The daemon normally does ~1-5/s in steady state. If p99 is high: is Bitcoin Core on a slow disk or swapping?
tx-index=1setup on HDD: makes GBT slow. SSD recommended.- Pruned node + old block request: doesn’t happen on the normal pool path, but backup/restore workflows can trigger it.
Fix: usually a disk bottleneck. iotop / iostat shows this quickly.
ZMQ
Daemon only uses poll-loop, no ZMQ
Symptom: job refresh runs poll-only because no ZMQ watcher is active — the
[node] zmq_hashblock_addr in config.toml is empty or missing (it is not an
env var). If a configured channel later drops, the log shows:
zmq channel closed — falling back to polling only
Fix: set in config.toml:
[node]
zmq_hashblock_addr = "tcp://127.0.0.1:28332"
And in bitcoin.conf:
zmqpubhashblock=tcp://127.0.0.1:28332
Bitcoin Core must be restarted after this change.
ZMQ connection refused / timeout
Bitcoin Core isn’t listening on the ZMQ port. Diagnosis:
ss -tlnp | grep 28332
# Expected output: bitcoind listens on 0.0.0.0:28332 or 127.0.0.1:28332
If nothing: the bitcoin.conf line is missing or Bitcoin Core wasn’t
restarted.
If 0.0.0.0: ZMQ is exposed — often necessary with a Docker bridge (daemon
and bitcoind in different containers). On a direct host
tcp://127.0.0.1:28332 should be enough.
Stratum / miner connections
Workers reconnect-loop every few seconds
Symptom: many session ended with shares_accepted = 0 in the logs. Many
miner-disconnect events in the notifier (or, with Phase 15 debounce, one every 30s).
Common causes:
- Wrong worker address: a V1 miner submits an address the ShareValidator
doesn’t accept. Logs show
mining.authorizewith a bad address. - VarDiff too aggressive: the initial diff was too high, the miner can’t
produce shares within the
target_seconds_per_sharewindow, retargets down quickly, and some firmwares don’t like rapid retargets. Fix: a highermin_diffor a lowerinitial_diffin[vardiff]. - Auth rate limit kicks in: at >
auths_per_secauth attempts per IP you get rejected. In the log:auth rate-limited. For legitimate cases with multiple workers behind NAT: raise the rate limit.
Miner can’t connect at all
# Is the pool port reachable?
nc -v pool.local 3333
# TLS port?
openssl s_client -connect pool.local:3334 -showcerts
If local is OK but external isn’t: firewall / NAT. The UPnP wizard in setup (Phase 11) can open this automatically, but only if the FritzBox / router allows UPnP. Some ISP routers have UPnP off by default.
External miner suddenly can’t connect (auto-banned)
A public IP that repeatedly trips the connect rate limit is auto-banned and
dropped before the handshake (default: 10 strikes / 60 s → escalating 5 min …
1 h). Loopback and private LAN are never auto-banned. Check the ban state
via /metrics (warppool_auto_bans_active, warppool_manual_bans) or the
Admin → Profile panel. Clear a manual ban with POST /api/admin/ratelimit/unban;
relax auto-ban via [ratelimit].autoban_* (or autoban_enabled = false).
Dashboard shows “API unreachable → HTTP 429”
The HTTP API is per-IP rate-limited (api_rpm, profile-driven; over the limit
→ HTTP 429 + Retry-After). Loopback and private LAN are exempt (the LAN
exemption was widened in v1.4.1 — before that, reaching the dashboard over the
box’s LAN IP with several tabs + polling could trip it). If you still see it
from a genuinely remote IP, switch to a larger profile (bigger api_rpm) or put
a reverse proxy with its own limits in front.
Miner is on the wrong protocol port
Each protocol has its own endpoint: Stratum V1 plain on 3333,
V1-over-TLS on 3334, Stratum V2 on 34254 (defaults; the connect
modal in the UI shows the exact host:port for all three). A miner in
SV2 mode pointed at 3333 fails its NOISE handshake against the
line-based V1 listener; a V1 miner on 34254 dies in the encrypted
framing. Since v1.0.22 both listeners detect this mismatch and log a
warning naming the right port — grep -i "point it at" in the pool log
finds it. (Real-world example: #60,
a Bitaxe in SV2 mode configured with the V1 port.)
TLS: handshake fails with BadCertificate
Symptom: the miner works on port 3333 but not on the TLS port; the daemon log shows, once per reconnect attempt:
WARN tls handshake failed peer=192.168.x.y:nnnnn error=received fatal alert: BadCertificate
The alert comes from the miner: its firmware verifies the server certificate and rejects the pool’s self-signed one. What works depends entirely on what the firmware offers (ESP-Miner family naming):
| Firmware TLS mode | Self-signed pool cert | What to do |
|---|---|---|
| No verification | ✓ works | nothing — connect to :3334 |
| TLS (Custom CA certificate) | ✓ works | download the cert from the connect modal (or GET /api/tls-cert), paste it as custom CA, and use a hostname from the certificate (umbrel.local, dvb-warppool.local) instead of a raw IP — esp-tls also checks the common name |
| TLS (System/Bundled CA) only — e.g. BitForge Nano firmware v1.5 | ✗ can never work | see below |
A bundled-CA-only firmware insists on a certificate signed by a public CA — that is the whole point of the check, so there is no self-signed workaround. Realistic options:
- Stay on plain
:3333for that miner. In a home LAN this is a perfectly reasonable trade-off (TLS on Stratum mainly guards against on-path tampering; the share data itself is not secret). - Serve a CA-signed certificate: if you own a domain, issue e.g. a
Let’s Encrypt certificate for
pool.example.com(DNS-01 challenge — the host doesn’t need to be publicly reachable), point the DNS record at the pool’s LAN IP, set[stratum] tls_cert_path/tls_key_pathto the issued files and connect the miner topool.example.com:3334. Renewal (90 days for LE) plus a daemon restart is your responsibility. - Ask the vendor for a firmware update — ESP-Miner mainline already ships the “TLS (Custom CA certificate)” mode, so forks usually pick it up over time.
A plaintext miner pointed at the TLS port produces a different error
(tls handshake failed ... received corrupt message-style) — that is a
forgotten SSL toggle, not a certificate problem.
Notifier doesn’t fire
notifier sink skipped: env var foo not set
At daemon start, the env vars for the sink aren’t present. With systemd:
[Service]
EnvironmentFile=/etc/default/dvb-warppool
With /etc/default/dvb-warppool as a chmod-600 file:
TELEGRAM_BOT_TOKEN=123:abc...
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
POOL_SMTP_PASSWORD=...
For Docker: via -e KEY=val or an env-file in docker-compose.yml.
Test button shows err
Click Test in the admin UI → err badge. The specific error shows as a
tooltip on the badge (hover). Common cases:
- Telegram:
bot not found— wrong bot_token.chat not found— wrong chat_id or the bot was removed from the chat. - Discord:
Unknown Webhook— webhook was deleted. - Slack:
invalid_payload— very rare; usually a typo in the webhook URL. - Email:
Could not resolve host— wrong SMTP host.authentication failed— wrong password env.unable to connect: connection refused— wrong port or firewall.
Spam from miner-disconnect events
Per-worker debounce is 30s (WARPPOOL_DISCONNECT_DEBOUNCE_SECS). If that’s
too noisy: set the env var to 300 (5min).
If a single worker keeps disconnecting: that’s a real problem on the miner
side — check the logs on the miner hardware (the Bitaxe web interface has
Logs; Antminer has SSH access).
Auto-update
Update fails: download/verify: sha256 mismatch
The release asset was changed between when you saw the release and
downloaded it (rare), or you passed the wrong sha256. For POST /api/admin/update,
always take the sha256 from the release notes file or from the forge
API itself — never type it manually.
Update applied but daemon keeps restarting (StartLimitBurst)
The Phase 8f rollback hook fired. The systemd journal shows:
OnFailure=dvb-warppool-rollback.service triggered
rollback.sh copies the .backup back and runs systemctl restart. If the
backup is also broken: install the old version manually or roll back from
a snapshot.
cosign verify-blob failed
When cosign_verify=true is set in the update request: the signature doesn’t
match the asset. Possible causes:
- The asset is authentic but the signature file is stale
- The asset has been tampered with
The default is cosign_verify=false — only use cosign if you’ve explicitly
configured the trust anchor (public key or Sigstore root).
Setup wizard
“Cookie not readable”
Most likely a permission issue. The setup wizard shows the exact path + suggested fix:
sudo usermod -a -G bitcoin warppool # one-time
sudo chmod g+r ~/.bitcoin/.cookie # after every Core restart
Cleaner: configure bitcoind with rpcauth=warppool:saltedhash and use
user/pass mode instead of the cookie.
UPnP test “no gateway found”
The router has UPnP off, or the daemon is bound to the wrong interface. Some ISP routers (Vodafone, some FritzBoxes with a locked-down user account) don’t allow UPnP from the outside.
Fix:
- Enable UPnP in the router web interface
- Alternatively: set port forwarding manually (3333/3334)
bitcoin.conf snippet suggests recommendations for keys that are already set
If the setup wizard says “add zmqpubhashblock=...” even though it’s already
there — usually the wrong conf path. Check bitcoin-cli -datadir=… getrpcinfo
to see the datadir that’s actually in use.
API / UI
/api/admin/* returns 401
Admin auth is configured but the token is missing from the request:
curl -H "Authorization: Bearer wpat_…" http://pool.local:18334/api/admin/notifier/sinks
Generate a token with dvb-warppool-cli token-create -n my-script — the raw
token is shown only once.
/api/admin/* returns 503 “auth disabled”
secrets.toml has no admin_password_hash or jwt_secret. Fix:
dvb-warppool-cli hash-password
and paste the output into secrets.toml.
UI doesn’t load — Failed to fetch /api/...
Direct --ui-dir mode or dev mode? In dev (pnpm dev), Vite runs with a
proxy /api → http://127.0.0.1:18334. In production: the UI is served
directly by the daemon via --ui-dir. Check the daemon startup log:
serving static UI from daemon ui_dir=/usr/share/dvb-warppool/ui
If not present: the --ui-dir flag is missing or the directory doesn’t exist.
Announcement banner doesn’t show up
The marquee announcement (Admin → Banner, since v1.21.0) is stored as two
pool_settings rows — text plus an RFC3339 expiry — and served in the banner
field of GET /api/overview. It is suppressed when the text is empty, the
expiry is missing/unparseable, or the expiry is already in the past — in that
case the field is omitted from the response entirely, not sent as an empty
string. So:
curl -s http://pool.local:18334/api/overview | grep -o '"banner":[^}]*}'
If that prints nothing, the announcement has expired (or was cleared) —
re-post it via Admin → Banner or
POST /api/admin/banner with {"text": "...", "duration_secs": 3600}.
Constraints the API enforces: at most 200 characters, no control characters,
and duration_secs between 60 (1 minute) and 7776000 (90 days). Ending it
early is DELETE /api/admin/banner; both writes land in the audit log
(banner.set / banner.clear).
Vendor probes (Phase 22)
Miner is discovered via mDNS but no telemetry
Possible causes:
| Vendor | mDNS discovery | Telemetry probe |
|---|---|---|
| Bitaxe | ✅ _bitaxe._tcp.local. | ✅ AxeOS HTTP /api/system/info |
| NerdNOS / NerdOctaxe | ✅ _nerdminer._tcp.local. | ✅ AxeOS-compatible |
| Antminer S19/S21/S23 | ❌ no mDNS | ✅ CGMiner socket 4028 — add manually in /admin/miners with vendor=antminer |
| Whatsminer M30S/M50 | ❌ | ✅ CGMiner socket — vendor=whatsminer |
| Avalon A12xx/A13xx standalone | ❌ | ✅ CGMiner socket — vendor=avalon |
| Avalon Q (home miner) | ❌ | ✅ CGMiner socket but its own field names — vendor=avalonq |
| Avalon A15xx (e.g. A1566) | ❌ | 🟡 try vendor=avalon — if the field naming differs, please file an issue with cargo run -- probe --vendor avalon --host … output |
| Braiins OS / OS+ | 🟡 via HTTP fallback when the hostname contains braiins* / bos-* | ✅ CGMiner socket 4028 with Braiins-specific field names (power_consumption_w, voltage, temp1..N) — vendor=braiins |
vendor=avalon and vendor=avalonq are separate adapters — one matches
MTavg/MTmax/GHS av, the other THSspd/TMax/Cur_Load. If you add
a Q with vendor=avalon, lots of None values come back.
Manual add command
curl -X POST -H "Authorization: Bearer wpat_..." \
-H "Content-Type: application/json" \
-d '{"host":"192.168.1.42:4028","vendor":"avalonq","label":"avalonq-1"}' \
http://pool.local:18334/api/admin/miners
Port 4028 is the CGMiner default. Some vendors reconfigure it — in that case, pass the correct port number.
Last resort
Daemon panicked and won’t come up
Look at the logs. Common cases:
- DB migration failed:
sqlx::migratethrows on an inconsistent schema - Config file broken: TOML parse error with line number
- Port taken: another pool / service on 3333
Rebuild storage DB from scratch
systemctl stop dvb-warppool
mv /var/lib/dvb-warppool/warppool.db /var/lib/dvb-warppool/warppool.db.broken
systemctl start dvb-warppool # daemon creates a fresh DB and runs migrations
You will lose:
- VarDiff snapshots per worker (every miner starts at initial_diff)
- Aggregated hashrate history (raw shares > 1h are evicted anyway)
- Audit log
Blocks-found is NOT the source of truth in the DB — those live in the
Bitcoin blockchain itself. The block list is lazily re-populated on the
first request via getblock RPCs.
See also
- Setup health checks — preventive diagnostics during setup
- Auto-update — update path + rollback
- Configuration Reference