stockfish.ross.gg

loadingโ€ฆ refresh

Autonomous chess bot on Board Game Arena. Friendly games only.

Bot rules

Stats

All Grandmaster Expert Advanced Intermediate Easy Beginner

Default difficulty is grandmaster-strength Stockfish, but players can set a lower difficulty with chat.

Games over time

โ€ฆ

Opponents by language

โ€ฆ

Engine usage

โ€ฆ

Opponents by membership

BGA premium vs. free, by finished game. Membership is read from the opponent's game-page profile, so it's only recorded for games played after this was added.

โ€ฆ

Active games

โ™ž gallery โ‰ก table
โ€ฆ

Open invites

โ€ฆ

Recent moves

Parallel engine race, winner picked by precedence: lichess-cloud-eval ยท stockfish.online ยท chess-api.com ยท rapidapi-stockfish-16 ยท stockfish-container ยท js-chess-engine (local DO) ยท random legal move.

โ€ฆ

Past Games

Before each opponent: โ— BGA Premium member ยท โ—‹ free member.

โ€ฆ

Bug? Suggestion? Drop a note. Optional contact field if you'd like a reply.

Past feedback
โ€ฆ

Technical details

Running a chess engine on Cloudflare Workers โ€” what worked, what didn't

Architecture

Each /bestmove call races every available engine in parallel inside the per-game Durable Object, capped at RACE_CEILING_MS=5000. When the ceiling fires (or all engines settle, whichever comes first) the DO picks a winner by a fixed precedence list โ€” the strongest engine that returned a legal move wins. Failed engines don't block the race, and every engine's result is recorded in the move log so you can see who carried each move.

Precedence (strongest โ†’ weakest): lichess-cloud-eval โ†’ chess-api.com โ†’ stockfish.online โ†’ rapidapi-stockfish-16 โ†’ js-chess-engine โ†’ random legal move. (stockfish-container sits between rapidapi-stockfish-16 and js-chess-engine but its binding is currently dormant โ€” see below.)

Engines we tried

EngineStatusNotes
chess-api.com Works External wrapper around Stockfish 17. Strongest in the race when it responds in time, but variable latency and the occasional flake mean we never trust it alone.
lichess-cloud-eval Works (cache-only) GET lichess.org/api/cloud-eval?fen=โ€ฆ&multiPv=1. Returns community-cached Stockfish evaluations, typically at very deep nominal depths (30–75+ ply). When it hits it's authoritative and wins the race; for most non-opening positions it 404s silently and the next engine wins. No auth, free.
stockfish.online Works GET stockfish.online/api/s/v2.php?fen=โ€ฆ&depth=โ€ฆ (max depth 15). Free, no auth. Added as a second hosted Stockfish source so the bot keeps playing real engine moves when chess-api.com rate-limits.
rapidapi-stockfish-16 Works (when key bound) POST chess-stockfish-16-api.p.rapidapi.com/chess/api, form-urlencoded fen=โ€ฆ body, x-rapidapi-key header. Server-side depth is fixed at 12 (the depth param is ignored). Returns bestmove + ponder only โ€” no eval, no continuation. Gated on RAPIDAPI_STOCKFISH_KEY being set; silently skipped otherwise.
wasm-stockfish Dropped โ€” doesn't fit stockfish-18-lite-single (~7 MB binary) compiled to WASM, hosted in a dedicated DO so OOMs couldn't take down anything else. Even with Hash=1, Threads=1, MultiPV=1, SyzygyPath=<empty>, UCI_AnalyseMode=false, the NNUE plus emscripten heap blew past the 128 MB DO memory cap on every call; cold-init also tended to exceed the 30 s CPU budget. Removed from the race after it never produced a move in production.
stockfish-container Dormant โ€” works, not worth the price Native stockfish in a Debian-slim container behind a small Node HTTP server (POST /bestmove, Hash=16, single thread, movetime clamped 50–2000 ms). Wired through a Cloudflare Container Durable Object, built locally with colima + binfmt for cross-arch (mac arm64 โ†’ cf amd64). We deployed it once: warm latency ~350 ms (beats chess-api.com), cold start ~9 s. Disabled because keeping a "lite" instance perpetually warm runs ~$245/mo โ€” roughly 40× what a $6/mo Linux VPS running the same server costs. Code stays in the tree; uncomment the [[containers]] blocks in wrangler.toml to re-enable.
vps-stockfish Planned The cheaper version of the container tier: a small Linux droplet running the same server.mjs as a systemd unit, behind nginx + Let's Encrypt. Worker calls over plain HTTPS. No cold starts, no per-second billing, no cross-arch build dance.
js-chess-engine Works (weak) Pure-JS alpha-beta search, level 3, ~30 KB. Always available inside the isolate with zero cold-start cost. Plays at roughly beginner strength but never times out.
random legal move Used more than expected Last-resort. In practice it ends up carrying a large share of moves whenever the hosted Stockfish APIs are all throttling or timing out together and lichess-cloud misses the position (everything past the opening book). When the bot picks a random move it also sends an in-game chat saying so, so the opponent isn't left wondering. See the "Engine usage" chart above for the current breakdown.

Platform constraints we hit

  • 128 MB DO memory cap. The single biggest constraint. Real Stockfish (even the lite WASM build) plus NNUE plus the emscripten heap won't fit.
  • 30 s DO CPU per request. Cold-init of a heavy WASM module can eat most of it before the first go movetime.
  • No runtime WebAssembly.instantiate of raw bytes. The WASM binary has to be bundled as a CompiledWasm rule in wrangler.toml and imported as a pre-compiled module.
  • Docker required for container builds. wrangler deploy shells out to docker build for any Cloudflare Container, so the container tier is blocked on having Docker installed on whichever machine runs the deploy.
  • Cron + DO alarms for scheduling. The bot polls BGA on a 5 s alarm loop inside its DO; a 1-minute Cron Trigger re-arms the alarm after deploys or evictions so the bot never falls silent.

What this means for play strength

On a good day chess-api.com or lichess-cloud responds first and the bot plays at roughly Stockfish-17 strength. On a bad day (or when both are throttling) the move falls to js-chess-engine at level 3, or all the way down to a random legal move when even that misses the 5 s ceiling. The bot is intentionally limited to friendly games for exactly this reason.

Recent findings

  • Per-FEN move cache. Each engine race result is stored against the position's FEN inside the per-game DO. A later turn (in any game) that reaches the same FEN reuses the cached move instead of racing again. Cached moves are still re-validated against BGA's current legal-move table before being played, so a stale entry that no longer fits (castling rights changed, etc.) silently falls through to a fresh race.
  • BGA HTML lag handling. Right after the bot plays, BGA's table HTML still reports the bot as the active player for a few hundred milliseconds. The dashboard and the tick loop now flip the turn locally as soon as selectCell succeeds and gate further engine work behind an awaitingOppMove flag, so the bot doesn't try to "play again" against its own move.
  • Chat is treated as untrusted input. The bot replies "I'm not sure." to every opponent chat message. No language-model parsing, no command handling โ€” chat is purely an injection surface and is answered the same way every time.
  • Auto-concede on repeated table errors. If the same table errors out three ticks in a row (HTML parse failure, repeated illegal-move rejection, etc.) the bot concedes that game and moves on, so one broken table can't stall the whole loop.
  • Stale invites get cleaned up. An open invite that BGA leaves stuck in setup for over 15 minutes is closed and re-created, so a peer that accepted-then-vanished doesn't pin a slot forever.
  • Finished-game reconciliation. BGA's finished list rolls off quickly, so a memo whose game ended between ticks can disappear from both play and finished. The bot looks each missing memo up by id (tableinfos.html?id=โ€ฆ) and only marks it finished after three consecutive misses, so a transient indexing gap doesn't drop the game from stats.
  • Realtime friendly-mode quirk. Setting BGA option 201 directly to 1 on a freshly-created realtime table demotes it back to async. Toggling 0 โ†’ 1 instead preserves the realtime flag โ€” that's now the order the bot uses when accepting a friendly invite.
  • Opponent inactivity detection. On realtime tables the bot watches BGA's per-player chess clock (reflexion.total); if the opponent overdraws and goes negative without playing, the bot concedes politely rather than spinning until a flag fall the server never reports.
Recent errors
โ€ฆ
Non-scored games (concedes, opponent quits, aborts, premium-gate voids)

Games that never produced a clean win/loss/draw, kept out of Past Games (and the win-rate stats) so odd terminations stay auditable here.

โ€ฆ