# 429 rate limit exceeded

*Personio · error message*

**In short:** Personio enforces roughly 200 requests per minute per credential and returns 429 on breach, with X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. Two details cause most real incidents. The authentication endpoint has its own limit of about 150 per minute, and exceeding it drops you to one request per second for the next 60 seconds. And the Documents and Applications endpoints are limited to roughly 20 requests per 60 seconds per company, an order of magnitude tighter than the general limit. An integration that re-authenticates before every call will hit the first one long before it hits the second.

## The limits, in the order they will bite you

| Scope | Approximate limit | Notes |
|---|---|---|
| General API | 200 requests per minute per credential | Returns 429 with rate-limit headers |
| Authentication endpoint | 150 requests per minute | Then throttled to ~1/second for 60 seconds |
| Documents, Applications | 20 requests per 60 seconds per company | Roughly ten times tighter |

The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. Reading `Remaining` and slowing down before it reaches zero is cheaper than recovering after it does.

## The mistake that causes most of these

Personio authenticates by posting credentials to an auth endpoint and receiving a bearer token. The naive implementation fetches a fresh token for each request, because that is the shape that most obviously always works.

In testing it does always work. In production it has two consequences:

1. **It doubles your request count.** Every operation becomes two calls.
2. **Half of them hit the endpoint with the strictest limit and the worst penalty.**

And the penalty is the part that hurts. Exceeding the auth limit does not just return 429 until the window resets. It drops you to roughly one request per second for the next 60 seconds. For a batch job that was moving at 200 per minute, that is a two-orders-of-magnitude slowdown arriving without warning.

**Fetch a token once, cache it for its lifetime, and refresh on a 401.** That single change eliminates the most common cause of 429s against Personio.

## Building around the Documents limit

Twenty requests per sixty seconds is not a limit you handle with retries. It is a limit you design around.

At that rate, pulling a thousand documents takes fifty minutes at best, assuming nothing else competes for the same allowance. An integration that treats document export as an ordinary loop will either run for hours or fail partway through with most of the work lost.

What works instead:

- Treat document export as a **long-running background job**, not a request-response operation.
- Make it **resumable**, so an interruption continues rather than restarts.
- **Track which documents have already been fetched**, so a rerun does not redo completed work.
- **Pace to the Documents limit specifically**, not to the general one.

## Idempotency, because retries are inevitable

Rate limiting guarantees that some logical operations become multiple HTTP attempts. If those operations are writes, the absence of an idempotency strategy means retries can create duplicates: two absence records, two uploaded documents, two applicants.

This is worse than the rate limit itself. A throttled sync is slow; a duplicated absence record is wrong data in an HR system that someone has to find and remove.

## What we do differently

- **Tokens are cached and refreshed on 401**, never fetched per request. Most Personio rate-limit incidents disappear with this alone.
- **Pacing is per endpoint class.** Documents get their own budget rather than sharing a global one, so a document export cannot starve an employee sync.
- **429 is treated as an expected operating condition**, with exponential backoff and jitter, and it is measured rather than alarmed on. We alert when work misses its window.
- **Writes are queued and idempotent.** A retry cannot create a second record.
- **Long jobs are resumable and report progress**, so a document export that takes an hour is a visible job with a completion estimate rather than a process that either finishes or does not.

## Frequently asked questions

### Why would anyone re-authenticate on every call?

Because Personio uses a bearer token flow where you post credentials to get a token, and the simplest correct-looking implementation fetches a fresh token per request. It works in testing, where request volume is low. In production it doubles your request count and spends half of it on the endpoint with the strictest limit and the harshest penalty. Fetch a token once, cache it, and refresh on 401.

### What is the auth endpoint penalty exactly?

Exceeding the authentication limit does not simply return 429 until the window resets. Further requests are accepted only at about one per second for the following 60 seconds, after which the normal allowance returns. That is a meaningful outage for a batch job: a sync that assumed it could authenticate freely goes from minutes to a crawl.

### Why are Documents so much tighter?

Documents and Applications carry file payloads and personal data, so they are limited far more aggressively, at roughly 20 requests per 60 seconds per company. Any integration that pulls documents in bulk has to be built around that number specifically. Pacing to the general limit and hoping is how a document export runs for hours and then fails near the end.

### Are the limits the same across all Personio APIs?

No, and assuming so is a common source of surprise. Limits vary by API generation and by endpoint. Treat the response headers as the source of truth for the endpoint you are calling rather than applying one number everywhere.

### What breaks if we just retry?

Duplicates, if the retried call was a write. Retrying an absence creation or a document upload without an idempotency strategy can produce two records where the user made one request. Rate limiting turns one logical write into several attempts, which is exactly when idempotency stops being optional.

## Integrations this affects

- [Personio ↔ DATEV](https://seamless.engineering/integrations/personio-datev/): Personio DATEV integration
- [Personio ↔ Workday](https://seamless.engineering/integrations/personio-workday/): Personio Workday integration
- [Personio ↔ Slack](https://seamless.engineering/integrations/personio-slack/): Personio Slack integration
- [Personio ↔ DATEV Lohn und Gehalt](https://seamless.engineering/integrations/personio-lohn-gehalt/): Personio DATEV Lohn und Gehalt integration

## Request a scoping call

Seeing this in production and want it to stop being your problem? We design, build, and permanently operate the pipeline, including the validation and retry logic that stops this class of failure reaching you. Fixed-price scoping quote within 48 hours.

- Email: hello@seamless.engineering
- Contact form: https://seamless.engineering/#contact
