Skip to content

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: chatplatform compiles 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.

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.