OutboundPacer
in package
Paces what the bot says *into* Discord, per channel, across every connector.
Discord\Http already queues per rate-limit bucket and caps concurrency, and
everything here goes through it — nothing in this project may hold a second
HTTP client. What that machinery cannot do is decline to make a request in
the first place, and the arithmetic changed when the platforms were merged:
- Two separate bots were two applications with two tokens, and therefore two 50-requests-per-second budgets. One bot has one.
- One message on a busy network fans out to every Discord channel following that room, across unrelated servers.
- Two connectors relaying into the same channel each stay under the limit on their own and breach it together, which is exactly the failure a per-sender limit cannot see.
So relayed traffic is throttled at the source, by destination channel, leaving the budget for the things a person is waiting on. Interaction responses deliberately do not come through here: Discord documents them as exempt from the global limit, so a command keeps answering while the relay is saturated.
Bursts are not the enemy — a token bucket lets ordinary chat through immediately and only slows a genuine flood. A channel out of tokens is skipped over rather than stalling the whole queue behind it.
Tags
Table of Contents
Constants
- CAPACITY : mixed = 5
- Discord's per-channel message sublimit: five in five seconds.
- PER : mixed = 5.0
Properties
- $capacity : int
- $clock : mixed
- $draining : bool
- $limiters : array<string, RateLimiter>
- $loop : LoopInterface
- $per : float
- $queue : array<int, Deferred}>
Methods
- __construct() : mixed
- enqueue() : PromiseInterface
- Runs `$send` as soon as the channel's budget allows, and resolves with whatever it returns.
- queued() : int
- How many sends are waiting, for logging and health checks.
- queuedFor() : int
- How many are waiting on one channel.
- drain() : void
- Sends whatever the budget allows, then re-arms a timer for the rest.
Constants
CAPACITY
Discord's per-channel message sublimit: five in five seconds.
public
mixed
CAPACITY
= 5
Not published as a header — it is a documented property of the message routes — so it is encoded here rather than discovered from a 429.
PER
public
mixed
PER
= 5.0
Properties
$capacity read-only
private
int
$capacity
= self::CAPACITY
$clock
private
mixed
$clock
= null
$draining
private
bool
$draining
= false
$limiters
private
array<string, RateLimiter>
$limiters
= []
$loop read-only
private
LoopInterface
$loop
$per read-only
private
float
$per
= self::PER
$queue
private
array<int, Deferred}>
$queue
= []
Methods
__construct()
public
__construct(LoopInterface $loop[, int $capacity = self::CAPACITY ][, float $per = self::PER ][, callable(): float|null $clock = null ]) : mixed
Parameters
- $loop : LoopInterface
- $capacity : int = self::CAPACITY
- $per : float = self::PER
- $clock : callable(): float|null = null
-
Defaults to
microtime(true); injected for tests.
enqueue()
Runs `$send` as soon as the channel's budget allows, and resolves with whatever it returns.
public
enqueue(string $channelId, callable(): PromiseInterface $send) : PromiseInterface
The caller gets a promise rather than a callback so a failure — a webhook that has been deleted, a channel the bot was removed from — still surfaces at the point that asked for the send.
Parameters
- $channelId : string
- $send : callable(): PromiseInterface
Return values
PromiseInterfacequeued()
How many sends are waiting, for logging and health checks.
public
queued() : int
Return values
intqueuedFor()
How many are waiting on one channel.
public
queuedFor(string $channelId) : int
Parameters
- $channelId : string
Return values
intdrain()
Sends whatever the budget allows, then re-arms a timer for the rest.
private
drain() : void
Head-of-line blocking is avoided by skipping over a channel that is out of tokens instead of stalling the whole queue behind it: one busy channel should not delay a quiet one in another server.