The separate limiters behind agent and social calls
Agent chat, generation and social posting sit on separate counters. Size each stage against its own limiter so a publishing 429 cannot stall generation.
A pipeline that dies on POST /social/posts can still have a full generate window. Versely does not keep one global RPM for "anything you do". Endpoint families have named counters, keyed independently, and a 429 on publishing is evidence about publishing, not about video generation.
If you size the whole worker to the tightest stage you will idle generation behind a social cap that does not apply to it. If you size it to the loosest stage you will 429 the tight one and retry the wrong thing. Read the family, then size that stage.
Four families, four counters
These are the named limiters that matter for an integration. Each one has its own Redis key prefix. Exhausting one does not decrement the others.
| Family | How it is keyed | Window | Cap | What it actually wraps |
|---|---|---|---|---|
| Generate (cost-sensitive) | Client IP | 60 seconds | 30 requests | Every /generate/* route, including image, video, lipsync, audio, upscale, story |
| Agent chat | Authenticated user | 60 seconds | 60 requests | POST /agentic/chat, /chat/stream, /chat/answer-now, /quick-generate, /plan/execute, /canvas-agent |
| Social post | Authenticated user | 60 seconds | 10 requests | POST /social/posts and POST /social/preview |
| Social sync | Authenticated user | 60 seconds | 5 requests | POST /social/accounts/refresh |
A fifth counter sits in front of all of them when you authenticate with an API key: per-key RPM, default 60, clamped 1 to 1000 at key creation. It is keyed on the key id, not the user and not the IP. A 429 from that layer never reaches the route. The JSON is { "error": "Rate limit exceeded", "retryAfter": … }. A 429 from a named limiter uses { "error": { "code": "429", "message": "Too many requests. Please try again later." }, "retryAfter": … }. The shape tells you which family rejected you; the headers tell you when to come back.
Two more facts that catch people out:
Generate is IP-based, chat and social are user-based. Two API keys from the same office IP share the generate window even if they belong to different accounts. Chat and social key on the authenticated user id, so two keys for the same user share those windows, and two keys for different users do not. A burst of chat turns from one account will not spend another account's chat budget. A burst of generates from one NAT will.
The headers you read are the last limiter that ran. A generate call is authenticated (per-key RPM headers, if you used a key) and then run through the generate limiter, which overwrites X-RateLimit-Limit / Remaining / Reset with the 30/min IP window. A status poll never hits the generate limiter, so with a key you see the per-key RPM instead. Log the path next to the headers or you will tune the wrong stage.
The windows are fixed. The TTL is armed on the first increment and is not slid forward by later hits. That used to work the other way; a polling client could push the expiry forever and never recover. It does not work that way now. X-RateLimit-Reset is a real opening time.
Generation versus publishing
A typical content worker does three things in sequence: generate the asset, maybe ask the agent to write a caption, then post or schedule. Those three steps are three counters.
Worked example, one minute, one user, one IP:
- 24 video generates. Generate remaining: 6. Social remaining: 10. Chat remaining: 60.
- 8 social posts. Generate remaining: still 6. Social remaining: 2. Chat remaining: 60.
- 40 chat turns while you iterate captions. Generate remaining: still 6. Social remaining: still 2. Chat remaining: 20.
Nothing in that minute "used up the API". The publish stage is close to its cap and the generate stage is not. If the worker is a single loop that treats any 429 as "back off everything", you will park 6 unused generate slots because social is tight. Split the worker:
- A generate producer that only calls
/generate/*and only backs off on the generate headers. - A publisher that only calls
/social/postsand only backs off on the social headers, at most 10 accepted posts per minute per user. - Caption or planning calls against
/agentic/chaton their own, with the 60/min user cap in mind.
GET /social/posts and account listing are not on the post limiter. POST /social/accounts/refresh is on the sync limiter at 5/min, which is the one that will bite a "reconnect all accounts on a timer" job. Connect the accounts in the product (connect a social account); refresh on demand, not on a one-second cron.
Publishing is also where credits and platform constraints meet the limiter. The post call can be under the 10/min cap and still fail because the destination rejected the payload. That is not a Versely 429. Do not apply retryAfter to it.
Agent chat sits on its own budget
The 60/min user cap is on the heavy chat endpoints, not on the whole /agentic tree. Model lists, conversation reads, memory reads and task listing are not on that limiter. Streaming and non-streaming chat share it: POST /chat and POST /chat/stream increment the same agentic_chat counter. /chat/answer-now does too, so a user who interrupts a long turn is spending the same budget as the turn they interrupted.
/quick-generate and /plan/execute are on it as well. A "plan then run" flow is two hits before any /generate call happens. Size the agent stage for turns, not for clips.
The MCP connector does not get a separate, softer budget. Tool calls from an MCP session hit the same named family as the equivalent REST route: a generate tool spends the generate window, a social post spends the social window. The 60/min chat cap applies to /agentic/chat and the other chat endpoints in the table, not to every MCP tool call. Picking CLI, MCP or the API changes who initiates the call, not which limiter that call lands on.
Skills and the agent UI will happily issue those turns for you. An unattended agent that retries a failed tool call in a tight loop is the classic way to empty the chat budget without producing a single clip. Cap agent retries in your own prompt or wrapper; the limiter will not distinguish a useful turn from a spin.
Sizing a pipeline against the tightest stage
Do the arithmetic on the stage, then put a queue in front of the tight one.
Generate-bound work (a catalogue of SKUs, each needing a clip) should target the 30/min IP generate cap, and should not share that IP with a status-poll storm from another process. Status polls do not take generate slots, but they do take per-key RPM. Give the poller a key if you want; give it a slower cadence either way.
Publish-bound work (the clips already exist, you are filling a week's calendar) should target 10 accepted POST /social/posts per user per minute. That is 600 an hour if you actually sustain it, which you will not, because destination APIs and review will get there first. The Versely cap is the ceiling, not the plan.
Agent-bound work (a human in the loop, or an automated multi-step task) should target 60 turns per user per minute and should treat tool retries as turns.
A single-process worker that generates, captions and posts in-line is fine at low volume. The moment one stage 429s, break it up. The signal is a 429 body whose retryAfter is small (a few tens of seconds) on one family while X-RateLimit-Remaining on a different family's recent 200 is still healthy. That is an independent limiter doing its job. Park the stage that returned 429. Let the others run.
The social media video generator is the product surface for the generate-then-publish loop if you are not ready to own the queues. The limiter map above is what you are owning once you take that loop in-house.
Credits are a separate budget from all of this. A generate call that is under the 30/min cap still costs credits; there is no free API allowance, and an empty balance fails closed rather than waiting on Reset. Check the balance before a batch, not after the third 403.
FAQ
Does a 429 on social mean I should stop generating?
No. Different counter, different key prefix, different cap. Finish the generate queue, persist the request IDs, and let the publisher drain when its window opens. Coupling the two is how you turn a 10/min post cap into a 10/min studio.
Why is my generate Limit 30 when I set the key to 120 RPM?
Because the generate route overwrites the rate-limit headers with the IP generate limiter after the key check. Your 120 RPM still exists and still rejects you if you exceed it; you just will not see 120 on a successful generate response. You will see it on routes that do not have a tighter named limiter, including GET /api/v1/status/:requestId.
Is the editor preview cooldown one of these limiters?
No. A free 480p preview pass (preview: true on an editor render) is a per-user cooldown of five seconds, returned as retry_after_ms, not as X-RateLimit-*. It is not an RPM cap, it does not share a counter with generate or social, and a single charge still applies only to the final export. Do not drive it with the HTTP backoff in this post.
Can I ask for a higher social cap?
The named caps above are what the middleware enforces. There is no documented per-plan concurrency ceiling and no documented per-plan multiplier on these numbers. Pace to the headers on the response you have. If you need more publishing throughput, run more users (the social limiters are per-user) or drain the queue over more minutes, not faster retries.