skip to content

What does getMaxParallelUsages() control on a BuildService, and how is it enforced?

level: seniorimportance: should knowfreq 35%

answer

  1. semaphore across whole build
  2. set on registration spec
  3. only counts declared usesService/@ServiceReference
  4. not intra-task synchronization
  5. unset = unlimited

basics

~10 s

It caps how many tasks may use the service at the same time. Set maxParallelUsages at registration to throttle access to a constrained resource; Gradle limits concurrent users to that number.

solid answer

~40 s

`maxParallelUsages` (set on the registration spec, backed by the service's `getMaxParallelUsages()`) bounds the number of tasks that may **concurrently** hold the service — turning the BuildService into a build-wide concurrency limiter for a constrained resource (e.g., a license-limited tool, a single-port server, a rate-limited API). Enforcement only works when consumers actually **declare** their usage via `usesService(provider)` or `@ServiceReference`; Gradle then schedules so no more than N declaring tasks run simultaneously. If left unset, there is no limit. It constrains task-level concurrency, not the threads inside a single task — within one task you still need your own synchronization. It's the supported replacement for hacks like custom `--max-workers` tweaking when you only need to throttle one resource.

code

kotlin · 7 lines
kotlin
val svc = gradle.sharedServices.registerIfAbsent("serial", SerialResource::class) {
    maxParallelUsages.set(1)
}

tasks.register("a") { usesService(svc); doLast { svc.get().use() } }
tasks.register("b") { usesService(svc); doLast { svc.get().use() } }
// task a and b will not run the service concurrently

go deeper

for a junior

Knows it limits how many tasks use the service at once.

for a middle

Can set it on the spec and pairs it with usesService.

for a senior

Explains it's only enforced for declared consumers, isn't intra-task synchronization, and unset means unlimited.

for a principal

Uses it to govern constrained shared resources across an org's builds and sets defensive thread-safety conventions.

## The problem it solves Gradle runs tasks in parallel (with `--parallel` and within a project where dependencies allow). Sometimes a shared resource can't tolerate unlimited concurrent access: a tool with N floating licenses, a daemon that binds a single port, an external API with a rate cap, or a directory only one writer may touch at a time. `maxParallelUsages` lets a **BuildService act as a semaphore** across the whole build. ## How you set it The service can override `getMaxParallelUsages()`, but the idiomatic way is to set it on the registration spec: ```kotlin val license = gradle.sharedServices.registerIfAbsent("toolLicense", ToolService::class) { maxParallelUsages.set(2) // at most 2 tasks use the tool at once } ``` ## How enforcement actually works The limit is enforced **only against tasks that declare they use the service**. Declaration happens via: - `task.usesService(provider)`, or - a `@ServiceReference` property on the task. Gradle's scheduler then ensures that at most `maxParallelUsages` such tasks execute concurrently — additional tasks wait for a 'slot' to free up. A task that obtains the service via `provider.get()` **without** declaring usage is **not** counted toward the limit, so the cap silently won't hold. This is the most common bug: people set the limit but forget to call `usesService`. ## What it does NOT do - It does **not** synchronize access within a single task. If your service method is called from multiple threads inside one task (e.g., a `WorkerExecutor` with several work items), you must make the service itself thread-safe. - It does **not** change the global worker count. It's a per-service constraint layered on top of normal scheduling. - Unset means **unbounded** — the default is no limit. ## Interaction with the Worker API and config cache The value participates in the configuration cache like other params/spec settings. When combined with the Worker API, the constraint still applies at the **task** granularity; if you fan out work inside a task, those work items share the single service instance and the task already holds one usage slot. ## Practical guidance Set `maxParallelUsages` to the true resource capacity, make the service body thread-safe regardless (defensive), and verify every consumer declares `usesService`. For a strictly serial resource, set it to `1` to get mutual exclusion across tasks.

  • You set maxParallelUsages to 1 but two tasks still hit the resource at once. Why?
    At least one task accessed the service via provider.get() without calling usesService()/@ServiceReference, so Gradle never counted it toward the limit. Declare usage on every consumer.
  • Does maxParallelUsages make your service thread-safe?
    No. It limits how many tasks use it concurrently, but within a single task multiple threads can still call it, so the service body must be thread-safe on its own.

Like a parking lot with N spaces: cars (tasks) that actually pull a ticket (declare usesService) are counted and may have to wait; a car that sneaks in without a ticket (calls get() undeclared) isn't counted and breaks the cap.

saying these in an interview costs you the question

  • Believing the limit applies even without usesService/@ServiceReference declarations.
  • Thinking it provides intra-task thread safety or changes the global worker count.

context