GuidesAPI ReferenceChangelogAPI StatusAPI PolicyGusto Security
Guides

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

  1. You have an access token for the company whose payroll you're updating. See Authentication and authorization.
  2. 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.
  3. 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_uuid
  • withholding_pay_period
  • skip_regular_deductions
  • fixed_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.

EndpointScope
Prepare a payroll for updatepayrolls:write employees:red
Update a payroll by IDpayrolls:write
Calculate a payrollpayrolls: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 calls

Prepare 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_uuid will conflict on the version token. The first request to commit succeeds. The second receives a 422 invalid_attribute_value error 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:

  1. Re-run prepare for the affected employee's page—the full 100-employee group that included this employee.
  2. 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>

  1. Call prepare for pages 1 through 10.
  2. 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

Related guides


Did this page help you?