REST and ratelimits
Every REST call funnels through one RequestHandler. The helpers on Client and on structures (createMessage, editChannel, member.ban, ...) all use it.
Every REST call funnels through one RequestHandler. The helpers on Client and on structures (createMessage, editChannel, member.ban, ...) all use it.
Issuing requests
You normally call the typed helpers, not the handler directly. To reach an endpoint Athena has not wrapped:
import { Routes } from 'discord-api-types/v10';
const channel = await client.requestHandler.request('GET', Routes.channel(channelID), true);Signature: request(method, url, auth?, body?, file?, route?, short?). For GET/DELETE, body keys become the query string. auth: true adds the Authorization header.
File uploads
await client.createMessage(channelID, { content: 'Look' }, { file: buffer, name: 'a.png' });
await client.createMessage(channelID, { content: 'Look' }, [
{ file: b1, name: 'a.png' },
{ file: b2, name: 'b.png' }
]);MIME type is sniffed from the filename. Multiple files become files[0], files[1], etc.
Discord's default upload limit is 10 MiB, enforced per attachment (boosted guilds and Nitro raise it). For interactions, interaction.attachmentSizeLimit carries the actual limit in bytes for the invoking context.
Ratelimit model
Discord limits per route bucket. Athena tracks each route with its own sequential bucket, so same-route requests run strictly in order, and updates buckets from the X-RateLimit-* headers.
Because routes keep their raw guild/channel/webhook IDs, one bucket exists per entity touched. Idle buckets (nothing queued, no request in flight, reset window elapsed) are swept every rest.bucketSweepInterval ms (default 5 minutes; 0 disables), so they do not accumulate for the lifetime of the process on large bots. A swept route simply relearns its limits from the next response's headers, identical to the first-ever request on that route.
On a 429: it reads Retry-After (or X-RateLimit-Reset-After), engages a global block if the limit was global, and re-queues the request in a priority slot so it keeps its place.
The invalid-request limit
Separately from per-route ratelimits, Discord counts 401, 403, and 429 responses against a hard limit of 10,000 per 10 minutes; exceeding it triggers a temporary Cloudflare ban of your IP (shared-resource 429s are exempt). Athena tracks this window and emits a warn at 80%. If you see that warning, find the loop producing the errors instead of retrying blindly.
Latency compensation
By default Athena tracks a rolling average of request latency and folds it into reset timing to absorb clock skew. Disable with rest.disableLatencyCompensation: true. rest.latencyThreshold (default 30000 ms) is where it warns about excessive latency.
Connection reuse
Athena defaults to a shared keep-alive HTTP(S) agent, so requests reuse connections instead of doing a fresh TLS handshake each time. This matters a lot at volume. Supplying your own rest.agent overrides it.
Error stacks
By default every request captures a caller stack so a failing call's DiscordRESTError / DiscordHTTPError points at your code (rest.fullErrorStacks: true). On high-volume bots that per-request capture is measurable CPU; set rest.fullErrorStacks: false to skip it. Errors then capture a stack only when they are actually constructed (the failure path) and still carry the method, route, code, status, and message.
Force-queueing until ready
rest.forceQueueing: true holds authenticated requests until the first shardPreReady, avoiding a startup race against Discord's identify queue.
Retry behaviour
- 429: re-queued after the reset interval (with a clamp so a missing or malformed
Retry-Aftercannot cause a tight loop). - 502: up to 4 retries with random 100 to 2000 ms backoff.
- other 5xx: thrown as
DiscordHTTPError. - 4xx: thrown as
DiscordRESTErrorwith Discord's code and message. See Errors.
Newer endpoints (2025-2026 API)
Paginated pins
const page = await client.getChannelPins(channelID, { before, limit });
// { items: [{ pinnedAt, message }], hasMore }Newest-pinned first; before is a timestamp, so pass the last item's pinnedAt to paginate. getPins, pinMessage, and unpinMessage use the new routes under the hood, and pinning requires the PIN_MESSAGES permission (enforced by Discord since 2026-02-23).
Bulk bans
bulkBanGuildMembers(guildID, userIDs, { deleteMessageSeconds, reason }) bans up to 200 users in one request and resolves { bannedUsers, failedUsers }. Requires both BAN_MEMBERS and MANAGE_GUILD.
Guild incidents and role counts
editGuildIncidentActions(guildID, { invitesDisabledUntil, dmsDisabledUntil })pauses invites and/or DMs for up to 24 hours (passnullto re-enable); the current state lives onguild.incidentsData.getGuildRoleMemberCounts(guildID)resolves a map of role ID to member count.
Message search
searchGuildMessages(guildID, query) takes a GuildMessagesSearchQuery filters object (content, channel_id, author_id, has, pinned, sorting, and more). Requires READ_MESSAGE_HISTORY plus the MESSAGE_CONTENT intent. While Discord is still indexing the guild it throws SearchIndexNotReadyError carrying a retryAfter; see Errors.
Voice channel status
setVoiceChannelStatus(channelID, status, reason?) sets or clears (pass null) a voice channel's status. Requires SET_VOICE_CHANNEL_STATUS, plus MANAGE_CHANNELS when the bot is not connected to that channel.
Invite target users
createChannelInvite accepts role_ids (roles granted when the invite is accepted) and targetUsersFile (a CSV of user IDs allowed to accept). Manage the list on an existing invite with getInviteTargetUsers, updateInviteTargetUsers (processing is asynchronous), and getInviteTargetUsersJobStatus.
Tracing
client.on('rawREST', (e) => console.log(e.method, e.url, e.resp.statusCode, e.latency));Endpoints
Endpoints holds URL builders (for example Endpoints.CHANNEL_MESSAGES(channelID)) and CDN helpers. They return paths, not method-typed calls; response types come from discord-api-types.
Tips
- Always
awaitREST calls; forgetting it loses the ratelimit feedback path. - Prefer bulk endpoints (
bulkEditCommands) over loops. - Check the cache before REST:
channel.messages.get(id)is microseconds, the REST round trip is tens of milliseconds.