Parallelize Payroll Updates
Cut update time on large-roster payrolls by splitting the work across concurrent requests.
For companies with large rosters—roughly 1,000 employees or more—a single update payroll request that covers every employee can take 10 to 20 seconds. You can cut end-to-end update time significantly by splitting employees across multiple concurrent requests to the same payroll.
This guide explains the supported concurrency limit, the recommended split strategy, and the rules you need to follow to avoid lock conflicts and version-token errors.
Prerequisites
- You have an access token for the company whose payroll you're updating. See Authentication and authorization.
- You have an existing oll for fore company. See Complete a regular payroll or or the relevant payroll type guide the relevant payroll type guide if you need to create one first.
- Your HTTP client supports concurrent requests and exponential-backoff retry.
How parallel updates work
Each update payroll request writes to individual payroll item rows—one row per employee. The API also acquires a brief coordination lock on the payroll at the start of each request. Requests that cover disjoint employee sets—sets with no overlapping employee_uuid values—run truly in parallel, because they never touch each other's payroll item rows.
A 409 response means another request held the coordination lock at the moment your request tried to acquire it. Because of this, there's a practical ceiling on how many requests can run simultaneously before contention outweighs the speedup.
Send at most 10 update requests simultaneously against the same payroll. Beyond that, lock contention cancels out the parallelism benefit.
Keep payroll-level fields out of parallel requests
Some fields apply to the whole payroll rather than to an individual employee. Don't include any of the following in a request that runs concurrently with other requests:
selected_funding_method_uuidwithholding_pay_periodskip_regular_deductionsfixed_withholding_rate
Changing any of these fields acquires a broader lock that covers every employee on the payroll, which serializes every request in the batch and eliminates the benefit of parallelism.
If you need to change a payroll-level field, do it in a separate sequential update request before starting the parallel update wave.
Required scopes
Call all three endpoints in this workflow with a company-level access token.
| Endpoint | Scope |
|---|---|
| Prepare a payroll for update | payrolls:write employees:red |
| Update a payroll by ID | payrolls:write |
| Calculate a payroll | payrolls:run |
Step 1: Fetch employee compensation data and version tokens
Call PUT /v1/companies/{company_id}/payrolls/{payroll_id}/prepare for each page of employees. The response returns the current compensation data and a version token for every employee already on the payroll. You'll need to pass the correct version in every update request.
The most common pattern is to page through all employees using the page and per query parameters:
curl --request PUT \
--url https://api.gusto-demo.com/v1/companies/{company_uuid}/payrolls/{payroll_uuid}/prepare \
--header 'accept: application/json' \
--header 'authorization: Bearer COMPANY_API_TOKEN' \
--header 'content-type: application/json' \
--data '{ "page": 1, "per": 100 }'If you already know the subset of employees you want to update, pass up to 100 employee_uuids instead:
--data '{ "employee_uuids": ["uuid-1", "uuid-2", "...up to 100"] }'The response contains an employee_compensations array, and each entry has a version field. Store the full employee_compensations array for the page—you'll send it back as the body of the matching update request in step 2.
Maximum employees for #prepare callsPrepare pages are intended for 100 employees. Sending update requests for those same 100-employee groups means each version token maps to exactly one update request. Smaller batches work, but they increase request count and overhead without reducing per-request latency.
Step 2: Send update requests concurrently
Call PUT /v1/companies/{company_id}/payrolls/{payroll_id} for each page. Send at most 10 requests in parallel, and make sure each request covers a disjoint subset of employees.
Here's the shape of one update request:
curl --request PUT \
--url https://api.gusto-demo.com/v1/companies/{company_uuid}/payrolls/{payroll_uuid} \
--header 'accept: application/json' \
--header 'authorization: Bearer COMPANY_API_TOKEN' \
--header 'content-type: application/json' \
--data '{
"employee_compensations": [
{
"employee_uuid": "uuid-1",
"version": "VERSION FROM PREPARE",
"hourly_compensations": [],
"fixed_compensations": [],
"paid_time_off": []
}
]
}'Two concurrent requests that include the same
employee_uuidwill conflict on the version token. The first request to commit succeeds. The second receives a422 invalid_attribute_valueerror for that employee.
Handle 409 lock contention
A 409 response with category: "invalid_operation" means another request held the payroll coordination lock at the moment your request tried to acquire it. This isn't a timeout—the request was rejected immediately.
The response body looks like this:
{
"errors": [{
"error_key": "base",
"category": "invalid_operation",
"message": "This payroll is currently being processed by another request. Please try again later."
}]
}Update payroll sets absolute values, not increments, so retrying the exact same request body is safe. Retry with exponential backoff.
async function updateWithRetry(url, body, headers, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(body),
});
if (res.status !== 409) return res;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 30000);
await new Promise(r => setTimeout(r, delay));
}
throw new Error('Max retries exceeded');
}Recover from a stale version
If a version token is no longer valid, the API returns a 422 for the affected employee only. Other employees in the same request are unaffected.
{
"errors": [{
"error_key": "base",
"category": "invalid_attribute_value",
"message": "Supplied Version (abc123) is invalid.",
"metadata": { "entity_type": "Employee", "entity_uuid": "..." }
}]
}To recover:
- Re-run prepare for the affected employee's page—the full 100-employee group that included this employee.
- Re-send the update for that page using the new version tokens from the prepare response.
You don't need to re-send other pages—their version tokens aren't affected.
Step 3: Call calculate after every update completes
Call PUT /v1/companies/{company_id}/payrolls/{payroll_id}/calculate only after every update request has returned a success response (or a final retry failure).
If you call calculate while update requests are still running, the payroll is calculated on incomplete data. There's no error—the calculation uses whatever values are committed at that moment.
Here's the correct sequencing for a 1,000-employee payroll:<paragraph></paragraph>
- Call prepare for pages 1 through 10.
- Send update requests for pages 1 through 10 concurrently (N ≤ 10) and wait for all 10 to compl
Quick reference
Endpoints used in this guide
| Purpose | Method | Path |
|---|---|---|
| Fetch compensation data | PUT | /v1/companies/{company_id}/payrolls/{payroll_id}/prepare |
| Update a payroll | PUT | /v1/companies/{company_id}/payrolls/{payroll_id} |
| Calculate a payroll | PUT | /v1/companies/{company_id}/payrolls/{payroll_id}/calculate |
Related guides
Updated 10 minutes ago