Entry order budget
Exchanges cap how many orders an account may send per window. Binance USD-M, for example, allows 300 per 10 seconds and 1200 per minute. A place and an amend cost one unit each; a cancel costs nothing.
The entry budget keeps the bot under those caps by making entry orders wait instead of refusing them. Exits are never held.
The limits are read from the exchange at startup, not hard-coded, so the windows and numbers are whatever that market publishes. They are frozen for the run: a raised or lowered account limit takes effect at the next restart. The examples below use USD-M's numbers.
How an entry is admitted
Three conditions must all hold:
- Shared allowance — a token bucket for the whole bot, refilling 15 sends per second with a burst of 60. A token goes to the order that has been waiting longest, not to whoever asks first.
- The bot's own send count — exact, checked on every send against the entry line of each window the exchange declares (on USD-M, 210 of the 300 per 10 seconds and 1,020 of the 1200 per minute). The exchange's counters only update when a response arrives, so this is what bounds a burst sent inside one round trip.
- The exchange's reported counts — what it says the account has used. This includes what the local count leaves out: this bot's own exits, and any other process on the same API key.
If any condition fails, the order waits and is retried on the next tick, or every 100 ms if no ticks arrive. One exception: after an API pause lifts, only a tick may send a waiting entry, so a quiet symbol never sends a price from before the pause.
What waits and what does not
| Operation | Behaviour |
|---|---|
| Edit of a resting entry | Waits until sent, withdrawn, or the order is gone. Sends at the latest price the strategy asked for |
| New entry | Waits up to placeMaxWaitMs (2 s), then is skipped — under any hold, an API pause included |
Take-profit, stop-loss, virtual SL, emergency close, CloseAll | Never held, never refused by the budget |
| Cancel | Never held by the budget |
The budget is the only thing described on this page. An API pause (request weight at 85%, or an IP ban) still stops entries and cancels while it lasts, on every venue, exactly as before. Exits go out during a pause too.
The share the entry lines leave unused is the exit reserve — on USD-M, 90 orders per 10 seconds. The bot warns when open positions exceed the book size that reserve is sized for, half the reserve of the shortest window: 45 on USD-M (22 on a hedge account), 15 on Spot, 90 on COIN-M. The startup line prints the number.
One bot process per API key
The bot's own send count (condition 2) is per process. Two bots on one key each admit 210 per 10 seconds against a hard 300, and only condition 3 lets them see each other — one response late, and reading zero right after a window rolls. Run one bot per key, or lower lineShort and lineLong on both.
First moves
An order that has not sent for 10 seconds may go without taking a shared token, provided the counts have room. This lets a market-wide burst leave at once rather than draining out at 15 per second.
This is a rolling window, not a one-time allowance. The condition is only "has this order sent in the last 10 seconds":
- an order sending steadily is a repeat every time, and queues for a token;
- the moment it goes quiet for 10 seconds, its next send is a first move again;
- an order that moves once a minute is therefore always a first move and never queues for a token. It still needs room on the counts, and it can wait behind an earlier first move when the bucket is empty.
The rule needs a short window to bound it. On a market that publishes only a minute window — Binance COIN-M — it is off, and every send takes a token; the startup line then says first moves off (no short window).
It is tracked per entry order — strategy, symbol and slot — so it follows the order, not the task. A per-strategy restart inside a running bot does not hand a still-active order a fresh token-free send. A process restart does: the record lives in memory, so after a restart every order is a first move and the bucket is full, while the exchange's own counters are not reset. The bot can then offer up to a full 10-second line on top of whatever the old process sent.
Per-order pacing
Each resting entry carries its own small credit. There is no interval and no quota — it is a bucket:
- it holds at most 20 credits and starts full;
- each send spends one;
- it refills continuously at one per 250 ms, whether or not the order is moving, up to the cap of 20.
So 20 is the largest burst an order can make back to back, not an allowance per minute or per anything else. Spend all 20 and the order is then limited to 4 sends per second, which is the refill rate. Recovering a full 20 from empty takes 5 seconds of not sending.
An example: an order repriced 20 times in a row has emptied its bucket and drops to one edit every 250 ms. Leave it alone for 2 seconds and it has 8 credits again, enough for another 8 back-to-back edits.
The credit follows the order, and is not reset by the strategy holding off. Alternating edit and hold does not refill it faster — only time does. It starts full again when the order itself is gone, filled or cancelled, because the next order in that slot is a new order.
An order repriced once a second never touches this. What it bounds is a single order being repriced continuously, which without it would drain the shared bucket and starve every other symbol.
Both this and the queue apply to any send of a resting entry, whatever the strategy is doing — placing it, following the book, widening a spread, resizing. The budget sees an entry order being sent; it does not know or care why.
Supported exchanges
The budget runs wherever the adapter can read the exchange's own order limits.
| Exchange | Market | Published order windows | Entry orders are |
|---|---|---|---|
| Binance | USD-M futures | 300 / 10 s and 1200 / min | held by the entry budget |
| Binance | COIN-M futures | 1200 / min only | held by the entry budget |
| Binance | Spot | 100 / 10 s and 200,000 / day | held by the entry budget |
| Bybit | linear, inverse | — | refused by the order-rate gate |
| OKX | — | — | not rate-limited by the bot |
| Hyperliquid | — | — | not rate-limited by the bot |
| HTX | — | — | not rate-limited by the bot |
| Gate | — | — | not rate-limited by the bot |
| Bitget | — | — | not rate-limited by the bot |
On Binance the limits come from that market's exchangeInfo — /fapi for USD-M, /dapi for COIN-M, /api for Spot — so each market gets its own windows and numbers, and the lines above apply to whatever windows it publishes: lineShort to a window of 10 seconds or less, lineLong to a longer one. On Spot the 10-second line is 70 of 100, so the bot's own count is what holds entries there most of the time; that is a wait, not a refusal. Spot's day line (170,000 of 200,000) is reachable by a busy bot; once it binds, entries wait until sends age out of a 24-hour window, and the bot warns once a minute per symbol that a long window's line is holding. If a market publishes no order limits, or one the bot cannot read, it logs a warning and falls back to the gate rather than guessing.
Bybit is the only venue still running the order-rate gate. It is the only other exchange whose adapter tracks the account's order usage at all.
The five below it track nothing, so no local mechanism throttles entries there: the bot sends what the strategy asks for and the exchange's own rejections are the only limit. That is not a safer position than the gate — it is an unguarded one — and it is why those venues are not recommended for high-frequency strategies.
The first line after startup says which mode you got, and with which windows, shortest first:
entry budget active: 10s×300, 1m×1200 (entry lines 210 / 1020; R=15/s B=60 C=20 T=250ms placeMaxWait=2000ms; positions warn above 45)entry budget active: 1m×1200 (entry lines 1020; R=15/s B=60 first moves off (no short window) C=20 T=250ms placeMaxWait=2000ms; positions warn above 90)no OrderBudgetSpec for Bybit: legacy order-rate gateThe order-rate gate
This is what runs where there is no entry budget, and what ran everywhere before it. It is driven entirely by the counts the exchange returns on its responses, so it only knows what the venue has already told it.
It refuses. When the account's reported usage of a window reaches its threshold, an entry place or edit is rejected locally, without reaching the exchange. The strategy sees a "too many orders" error, and the resting order stays where it was — at whatever price it already had. The strategy has to ask again on its next tick.
| Window | Refuses entries at |
|---|---|
| 10 seconds or shorter | 60% of the limit |
| Longer than 10 seconds | 85% |
| Request weight | 85% |
It pauses. At 85% of any window, or of request weight, the bot pauses: resting entry orders are cancelled, no new orders go out, and trading resumes when that window has rolled — 10 seconds for a 10-second window, 60 for a minute or for weight. What the pause cancelled is re-placed as it lifts, spread over time so the rebuild cannot immediately re-breach the window it is recovering from. A warning goes out at 75%.
Exits are never refused or cancelled by it, exactly as under the budget. A stop-loss, a take-profit or a close always reaches the exchange.
The practical difference from the budget: a refusal loses the move rather than delaying it, and the pause takes the working book away to recover budget that cancelling does not actually free — cancels cost nothing against the order count. That is why venues that publish their limits use the budget instead.
Configuration
Optional, and applies to the entry budget only — the order-rate gate has no settings. Defaults apply when absent, and existing configs need no change.
{
"api": {
"provider": "Binance",
"entryBudget": {
"perOrderBurst": 20,
"perOrderDelayMs": 250,
"perSec": 15,
"burst": 60,
"lineShort": 0.70,
"lineLong": 0.85,
"placeMaxWaitMs": 2000
}
}
}| Key | Default | Meaning |
|---|---|---|
perOrderBurst | 20 | sends one resting entry order may make immediately |
perOrderDelayMs | 250 | then one send per this long, per order |
perSec | 15 | shared refill rate for the bot |
burst | 60 | shared bucket depth |
lineShort | 0.70 | share of a window of 10 s or less entries may use |
lineLong | 0.85 | the same for every longer window |
placeMaxWaitMs | 2000 | how long a new entry waits before being skipped |
0 switches off what a key names: perSec: 0 means no shared bucket, and entries are admitted on the counts alone; perOrderBurst: 0 means no per-order pacing; lineShort: 0 or lineLong: 0 leaves that class of window without a line — for the bot's own count and for the exchange's reported count, which is checked against the same numbers. Two keys read differently: burst is floored at 1 while the bucket is on, and placeMaxWaitMs: 0 means a new entry goes only if it can go on the tick it was asked for, and is skipped otherwise. The bound is checked whenever an entry is held, and once more when an entry that was held is finally admitted, so one that was never held goes whatever its age and one that sat through a pause does not. Negatives are treated as 0. Applied at restart, like everything under api.
WARNING
lineShort and lineLong decide how much of the exchange's budget entries may spend. What they leave is what a stop-loss or emergency close draws on. Raising them trades exit headroom for entry throughput. Setting both to 0 together with perSec: 0 leaves the venue with no local order-rate limit at all — the old gate does not come back — and the bot warns at startup.
Monitoring
Three lines appear at the default info level, each meaning something is wrong:
[strat/SYMBOL] entry held: the account's reported order count is at an entry line — ...
[strat/SYMBOL] entry held at the 1d entry line (170000 of the venue's 200000): ...
[strat/SYMBOL] 46 open position legs bot-wide, above the 45 the exit reserve is sized forThe first means the account's reported count is at an entry line. This bot's own exits count towards it — they are not in the local count — and so does any other process on the key, so it can fire with nobody else on the key when exits are busy. It also goes to Telegram once per episode. The second means a long window's own line is holding — on Spot the day line — once a minute per symbol. The third counts live positions only, so a paper-only bot never prints it.
Ordinary waiting is logged at debug. To see it, set the bot's log level to debug (log.level in the config) — at info the grep below counts nothing:
tail -f ~/.tradectl/logs/<bot>/<bot>_$(date +%F).log | grep --line-buffered "entry seam"[shot/BTCUSDT] entry seam: slot _ held (Bucket), waited 1.2s
[shot/BTCUSDT] entry seam: slot _ granted after 2.9s
[shot/ETHUSDT] entry seam: placement for slot _ waited 2.1s > 2s, skipped| Reason | Meaning |
|---|---|
Bucket | waiting for a shared token |
Line(0) | the shortest window's count is at its line (10 s on USD-M and Spot; the minute on COIN-M) |
Line(1) | the next window's count is at its line (the minute on USD-M; the day on Spot) |
Allowance | this order is sending faster than its own pacing allows |
Venue | the exchange reports the key at a line |
Holds against grants gives the wait rate:
LOG=~/.tradectl/logs/<bot>/<bot>_$(date +%F).log
echo "held: $(grep -c 'entry seam: slot .* held' $LOG)"
echo "granted: $(grep -c 'entry seam: slot .* granted' $LOG)"
echo "skipped: $(grep -c 'entry seam: placement .* skipped' $LOG)"The third line is new entries the budget dropped past placeMaxWaitMs. It is the one to watch: it separates a budget that is losing entries from a strategy that keeps asking and eventually sends.
The same counters — holds by cause, queue depth, longest wait, sends, dropped placements — are reported to the platform with the bot's metrics and exposed as get_api_metrics through the MCP server when the mcp feature is on. They are not on the bot's control port, so the Lab does not show them.
Paper and backtest
- Paper runs the per-order pacing, the waiting and coalescing rules and the
placeMaxWaitMsbound, so what a strategy sends and when follows the same rules as live. It takes nothing from the shared bucket and records no sends — paper orders never reach the exchange — so a paper run overstates the entry throughput a live bot would get under a storm. - Backtest and replay are unaffected.