Access tokens obtained through the Client Credentials flow are subject to per-application limits in addition to your tenant's monthly allotment.
These limits exist to protect your tenant. The monthly allotment is shared by every integration running against your Rhythm tenant, so a single misbehaving application can exhaust it and leave every other integration unable to authenticate. Per-application limits contain that failure to the application causing it.
| Limit | Default | Window |
|---|---|---|
| Per application, per hour | 2 tokens | UTC clock hour |
| Per application, per day | 4 tokens | UTC calendar day |
| Per tenant, per month | 1,000 tokens | UTC calendar month |
Windows are calendar-aligned in UTC, not rolling. The hourly window resets at the top of each UTC hour, and the daily window resets at 00:00 UTC — not 24 hours after your first request.
A correctly designed integration requests one token per day and reuses it, so the daily limit is roughly four times what normal operation requires.
When a limit is exceeded, the token request is rejected and no token is issued:
{
"error": "invalid_request",
"error_description": "M2M access token quota exceeded (max 2 per hour). Access tokens are valid for 24 hours - cache and reuse them."
}The description states which limit was reached.
A rejected request affects the two limits differently, and the difference matters:
| Rejected request counts? | |
|---|---|
| Per-application hourly and daily limits | Yes — these count requests, not tokens issued |
| Your tenant's monthly allotment | No — only tokens actually issued are counted |
If an integration requests tokens in a loop and is rejected, those rejections cost you nothing from your monthly allotment. Only the tokens actually issued count against it.
This is deliberate: the per-application limits exist so that one misbehaving integration cannot drain the allotment shared by every other integration on your tenant.
Retrying still works against you. Because the hourly and daily counters record requests, each retry pushes your application further past its limit and delays the point at which it can obtain a token again.
An integration retrying in a loop typically receives only 2 tokens in a day — fewer than the 4 it would have received by not retrying at all.
Treat a quota rejection as non-retryable. It is not transient and it will not succeed on retry.
This is the opposite of a 429 Too Many Requests response, where retrying with exponential backoff is the correct behavior. A 429 means slow down; a quota rejection means you already hold a token you should be reusing.
- Request an access token
- Cache it securely in memory or a server-side store
- Reuse it for every API call until it is close to expiring
- Request a new one only then — tokens are valid for 24 hours
- If a token request is rejected for quota, stop requesting and surface the error
Common causes of exceeding the limits:
- Requesting a new token on every API call
- Requesting a new token each time a scheduled job runs
- Requesting a new token per end user
- Running several instances or workers that each request their own token without a shared cache
- Retrying automatically after a rejection
If your integration runs as several instances, containers or workers, they all share one application's limits — the limits apply to the application, not to the process.
Either share a cached token between them, or request a higher limit for that application.
Limits are configured per application and can be raised where there is a legitimate need — for example an integration that genuinely requires more than four tokens a day across coordinated workers.
Contact Rhythm support with the application name and your expected token usage.
- Tokens are cached and reused for their full 24-hour lifetime
- A single token is shared across instances, or a higher limit has been requested
- Quota rejections are surfaced, not retried
-
429responses are retried with backoff — quota rejections are not