Discord library decision — phase 1a-0 spike¶
- Authors
- Matt Cockayne, Claude Opus 5 (AI drafting assistant)
- Date
- 2026-07-27
- Status
- APPROVED — 2026-07-27
- Implements
- R-ARCH-12, R-ARCH-13, R-ARCH-14; §3.3 of the requirements.
Decision¶
github.com/disgoorg/disgo backs chat-platform-discord. Validated live on 2026-07-27 — see
the resume verification below.
It wins on reconnect and resume — the criterion placed first in the rubric because it is the actual production failure mode — and on rate-limit handling, observability and API currency. It loses on footprint, connect latency and adapter verbosity, all of which are real and all of which matter less than silently dropping messages.
What was run¶
All three candidates implemented the identical vertical slice against a draft contract (R-ARCH-12): connect to the gateway, receive a message on an allowlisted channel, create a thread on it, post a reply, add a reaction. All three completed it live against the test guild.
The driver imports no provider package beyond a blank import for init() registration.
Scorecard¶
Rubric order is the requirements' order — deliberately not weighted toward what is easiest to measure.
| # | Criterion | discordgo | disgo | arikawa |
|---|---|---|---|---|
| 1 | Reconnect & resume | op 6 ✅, op 9 ✅, no resume_gateway_url |
op 6 ✅, op 9 ✅, EnableResumeURL on by default |
op 6 ✅, op 9 ✅, no resume_gateway_url |
| 2 | Rate limiting | 9 files | 30 files, bucket-aware | 4 files |
| 3 | Contract fit | 108 lines | 166 lines | 167 lines |
| 4 | Privileged intents | ✅ live | ✅ live | ✅ live |
| 5 | Threads | ✅ live | ✅ live | ✅ live |
| 6 | Footprint | 3 modules / 23 pkgs | 10 / 53 | 4 / 45 |
| 7 | Test ergonomics | concrete Session |
interface-based Gateway |
concrete State |
| 8 | Observability | none | native log/slog |
none |
| 9 | API currency | v9 | gateway v10 | v9 |
| — | Connect latency | 453–844 ms | 1.42–1.47 s | 562 ms–1.06 s |
Why criterion 1 decided it¶
Since Discord API v10 the gateway supplies resume_gateway_url on READY — a specific endpoint a
client must reconnect to in order to resume a session. Only disgo implements it, and it is on by
default.
All three send op 6 RESUME and all three handle op 9 INVALID_SESSION — an earlier reading of this
spike wrongly reported discordgo as missing op 9; it handles it at wsapi.go:618 using numeric
opcodes rather than named constants, which is why a symbol grep missed it. That correction narrows
the gap but does not close it: resuming against the generic gateway URL rather than the supplied one
risks the resume being rejected, which degrades to a full re-IDENTIFY, which drops the events
buffered during the disconnect.
For this bot, dropped events are unasked questions and unseen moderation-relevant messages. A failure mode that is invisible — no error, no crash, just silence where a question should have been answered — is the worst shape of failure for a support bot, because nothing surfaces it.
Verified live — 2026-07-27
The source-level reading has been confirmed against Discord. A peer-side connection reset
was induced, the outage held open for twenty seconds, and two messages posted while the gateway
was Disconnected:
failed to read next message from gateway err="connection reset by peer"
opening gateway connection
sending Resume command
← received "resume-test-B-during-gap" ← replayed
← received "resume-test-C-during-gap" ← replayed
successfully resumed session_id=6131d33f70a6da8611a6ad14d91f4260
Session preserved, op 6 RESUME rather than a fresh IDENTIFY, every gap event replayed, recovery in roughly one second. The decision no longer rests on reading gateway source.
Getting the test right took three wrong attempts¶
Recorded because each failure looked like a library defect and was not, and the next person to write this test will hit the same wall:
| Method | What happened | Why it proved nothing |
|---|---|---|
CloseWithCode(4000) |
Status went Disconnected, no reconnect |
A local close is a deliberate shutdown. Declining to reconnect from it is correct. |
| Duplicate IDENTIFY → remote close 4005 | Resume attempted, Discord answered can_resume=false, fell back to IDENTIFY |
4005 invalidates the session by design, so a resume was never possible. It did prove the invalid-session fallback path works. |
| Closing our own TCP socket | Reconnect path entered, then stopped | Yields net.ErrClosed, which disgo explicitly reads as "we closed the connection ourselves. Don't try to reconnect here" — again correct. |
Only a failure originating at the peer exercises resume. The working method routes the gateway
through a local TCP relay via websocket.Dialer.NetDialContext — gorilla performs TLS itself over
whatever that returns, so relaying raw TCP is transparent — then drops the relayed connections with
SetLinger(0) to force a reset, and refuses reconnects for the duration of the outage.
This method belongs in the module's test suite, not just in this record. It is the only way to regression-test the property the library was chosen for.
What disgo costs¶
Stated plainly, because these are real:
- Heaviest footprint — 10 external modules against discordgo's 3. Acceptable for an application;
it is precisely why R-ARCH-9 keeps the contract module
free of any of it, which the spike confirms:
chatplatformcompiles against stdlib alone. - Slowest connect — roughly 1.4 s against discordgo's 0.5 s. Irrelevant for a long-lived daemon.
- Most adapter boilerplate — though see below; most of that is the contract's fault, not disgo's.
- Pre-v1 — minor breaking changes still occur. Contained to one module by the boundary.
Contract findings¶
The spike's second objective (§3.3) was validating the contract, and it produced three results.
The boundary holds¶
chatplatform compiles with stdlib dependencies only — R-ARCH-9 verified rather than asserted.
The driver runs all three providers with no provider-specific code, and optional capabilities
(Moderator, MemberInspector) resolved by type assertion for all three.
The read/act split makes shadow mode structural¶
Building a provider with ReadOnly: true yields no Actor at all, so the process cannot post
regardless of any logic above it. This is R-OPS-7 enforced
by the type system rather than by a runtime flag, and it is the strongest argument for keeping
R-ARCH-10 exactly as written.
Recommended change — an opaque ID type¶
The contract should carry an opaque ID type rather than string.
The 108-versus-166-line gap is not library quality; it is a type mismatch at the boundary. discordgo uses strings natively, while disgo and arikawa use typed snowflakes — so both pay roughly 55 lines of parse-and-convert boilerplate, and every parse is an error path that cannot fail in practice since the ids came from the platform in the first place.
Since the chosen library is one of the two typed ones, this is worth fixing before chat-platform
ships. It thins the disgo and arikawa adapters and thickens a hypothetical discordgo one — the right
trade, given the contract should not privilege whichever library happened to pick strings.
Smaller finding — no channel-level post¶
The contract exposes only ReplyInThread, which is correct for production
(R-ANS-10 puts every answer in a thread) but meant the
spike could not seed its own test message and had to post via REST directly. Worth a deliberate
decision rather than an accident: either the contract grows a narrow channel-post method for
operational messages, or test scaffolding stays outside it.
Test-ergonomics note¶
Testing a chat bot needs either a second identity or a deliberate bot-filter bypass, since a bot's
own messages are filtered by R-ANS-3. The spike added a
AllowBots config field for exactly this. Design for it rather than bolt it on — and keep it
out of any code path a production build can reach.
disgo's interface-based Gateway is a genuine advantage here: it is the only candidate whose
gateway can be substituted without a network, which matters for the conformance harness
R-ARCH-3 requires.
Rejected¶
discordgo — the incumbent, smallest footprint, cleanest mapping, fastest connect, and by far the
largest community. Rejected on criterion 1: no resume_gateway_url, no tagged release in fourteen
months, still defaulting to API v9. Ecosystem gravity buys less than usual here because the surface
is wrapped once and never seen again.
arikawa — the cleanest architecture of the three, and its independent gateway and API client map well onto the read/act split. Rejected for the same resume gap as discordgo, without discordgo's compensating footprint or community advantages.
Artefacts¶
Discarded per R-ARCH-14. The spike code is evidence, not a foundation; the chosen provider is written properly, test-first, in Phase 1a.
Carried forward into that phase: the contract shape (with the ID change), the read/act split, the
optional-capability pattern, and the AllowBots test affordance.