Rate Limits
To ensure fair usage and system stability, Priostack limits how many requests each client IP address may send. The limit is applied by the firewall in front of the API, across all paths, whatever API key the requests carry.
Current Limits
| Limit | Value |
|---|---|
| Requests per minute | 300 per client IP |
| Window type | Sliding 60-second window |
| Scope | Per client IP address (not per API key), across all paths. Workers behind one IP share it. |
| Job activation | Not exempt: every POST /api/v1/jobs/activate counts as 1 request, and it answers at once (there is no long polling) |
| Not counted | The agent network transports /mcp and /rpc, operator paths, and GET requests on public pages from recognised search crawlers |
| Over the limit | 429 and a strike. Three strikes ban the IP for 2 hours, doubling on each repeat offence up to 7 days |
Rate Limit Headers
Every /api/ response that reaches the API includes the following headers:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit |
Maximum requests allowed per window | 300 |
X-RateLimit-Window |
Window duration in seconds | 60 |
X-RateLimit-Reset |
The next whole minute, as a Unix timestamp in seconds. Advisory only: the real window slides, so it does not tell you when your own requests age out | 1746456780 |
No header reports how many requests you have left.
# Inspect rate limit headers
curl -i -H "X-API-Key: your_key" \
https://priostack.com/api/v1/topology
HTTP/1.1 200 OK
X-RateLimit-Limit: 300
X-RateLimit-Window: 60
X-RateLimit-Reset: 1746456780
Content-Type: application/json
...
Handling 429 Responses
When the limit is exceeded, the firewall answers before the request reaches the API: HTTP 429 with a plain-text body, no JSON, no Retry-After and no rate-limit headers.
HTTP/1.1 429 Too Many Requests
Content-Type: text/plain; charset=utf-8
Too Many Requests
Rejected requests still count in the window, so retrying at once keeps you limited, and every 429 is a strike. After three strikes the IP is banned for 2 hours (doubling on each repeat, up to 7 days), and during a ban every request answers 403 with an empty body. Stop sending and wait about 60 seconds. A request that got 429 never reached the API, so it is safe to send again.
Retry Logic in Go
A retry must build a fresh *http.Request for each attempt: a request whose body was already read sends nothing the second time.
// callWithRetry builds a fresh request for every attempt, so a POST body is
// never sent empty on a retry. A 429 means the request never reached the API.
func callWithRetry(ctx context.Context, newReq func(context.Context) (*http.Request, error)) (*http.Response, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := newReq(ctx)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests {
return resp, nil
}
resp.Body.Close()
// The 429 has no Retry-After, the window slides over 60 s and
// rejected requests count too: wait a full window.
wait := 61 * time.Second
log.Printf("Rate limited. Waiting %s.", wait)
select {
case <-time.After(wait):
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, errors.New("exceeded max retry attempts")
}
// Usage: the body is rebuilt from the same bytes on every attempt.
func startOrder(ctx context.Context, orderID string) (*http.Response, error) {
body, err := json.Marshal(map[string]interface{}{
"bpmnProcessId": "order-processing",
"variables": map[string]interface{}{"orderId": orderID},
})
if err != nil {
return nil, err
}
return callWithRetry(ctx, func(ctx context.Context) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://priostack.com/api/v1/process-instances", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", os.Getenv("PRIOSTACK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
return req, nil
})
}
Retry Logic in Python
import time
import requests
def call_with_retry(url, headers, payload, max_attempts=5):
for attempt in range(max_attempts):
# requests builds a fresh request, body included, on every call.
response = requests.post(url, headers=headers, json=payload, timeout=15)
if response.status_code != 429:
return response
# The 429 has no Retry-After, the window slides over 60 s and
# rejected requests count too: wait a full window.
print("Rate limited. Sleeping 61s...")
time.sleep(61)
raise Exception("Max retry attempts exceeded")
Tips for High-Volume Usage
1. Pause Between Empty Polls
POST /api/v1/jobs/activate answers at once, with {"jobs": []} when nothing waits: there is no long polling. Sleep 1 to 5 seconds after an empty or failed poll, and ask for several jobs per call with maxJobsToActivate. A worker that pauses 2 seconds between empty polls sends about 30 requests a minute.
# One poll; pause 2 s before the next one when the answer is empty
curl -X POST "https://priostack.com/api/v1/jobs/activate" \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json" \
-d '{"type": "my-worker", "maxJobsToActivate": 10}'
2. Pace Instance Starts
There is no batch endpoint for starting instances. Start them one call each with POST /api/v1/process-instances: each call costs 1 credit and counts toward the 300 requests per minute, so spread large runs over time.
3. Cache Process Definitions
Avoid calling GET /api/v1/process-definitions on every request. Start instances by bpmnProcessId, which does not change: a changed redeploy creates a new version, and new starts always use the newest one.
4. Monitor Your Usage
No header reports your remaining requests, so count them in your application per outgoing IP, log a warning above 240 a minute (80% of the limit), and throttle before you reach it.