Fundamentals
Rate limits & quotas
Two different mechanisms: rate limits stop a loop from hammering the API, quotas bound what a month can cost. They fail differently and you recover from them differently.
Rate limits
Counted per token, across the REST API and the MCP server together — using both does not double your allowance.
| Scope of the limit | Allowance | Applies to |
|---|---|---|
| Per token | 120 requests / minute | Everything |
| Per token | 20 replies / minute | Comment replies specifically |
| Per IP | 60 failed authentications / minute | Unrecognised tokens |
Exceeding one returns 429. Wait and retry — nothing is consumed and nothing is broken.
Why replies are stricter
Quotas
Quotas are monthly and belong to the subscriber — the workspace owner — not to a token or a workspace. A plan that includes several workspaces shares one pool between them, so moving work around does not reset anything.
| Pool | Consumed by |
|---|---|
| Posts | Each publish that reaches at least one platform |
| X posts & replies | Each write to X — a tweet or a comment reply |
| X DMs | Direct messages sent and received on X |
| X comments | Comment data read from X |
| AI messages | Echo AI conversations in the app |
| Ads searches | Fresh competitor ad searches |
Publishing to Instagram, Facebook, YouTube, TikTok, LinkedIn, Threads, Bluesky, Pinterest and Google Business costs nothing on our side, so those posts are effectively unlimited. X is the one platform that charges per call, which is why it has pools of its own.
Hitting a quota
You get 429 with a message naming the pool and the numbers. Retrying does not help — the pool resets on the first of the month, or you upgrade the plan.
{
"error": "Monthly X posts & replies quota reached (1000/1000). X is a paid API — upgrade your plan to keep replying there."
}Reservations are refunded
Graceful degradation
Reading comments from X does not stop dead at the limit. Past 90% of the pool, Echoia widens the interval between live checks to fifteen minutes; past 100% it stops calling X and keeps serving the comments already stored. The inbox stays usable, it just stops getting newer.
Handling limits in code
Treat 429 as two different situations, told apart by the message: a rate limit is worth retrying after a pause, a quota is not.
async function call(path, init, attempt = 0) {
const res = await fetch(`https://app.echoia.io/api/v1${path}`, init);
if (res.status === 429) {
const { error } = await res.json();
// A quota names the pool; a rate limit does not. Retrying a quota is futile.
if (/quota/i.test(error)) throw new Error(error);
if (attempt >= 3) throw new Error("Rate limited, gave up after 3 tries");
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
return call(path, init, attempt + 1);
}
if (!res.ok) throw new Error((await res.json()).error);
return res.json();
}Seeing where you are
- Settings → Billing shows every pool with what has been used this period.
- Settings → Developers shows each token and when it was last used.