What does getMaxParallelUsages() control on a BuildService, and how is it enforced?
answer
- semaphore across whole build
- set on registration spec
- only counts declared usesService/@ServiceReference
- not intra-task synchronization
- unset = unlimited
basics
~10 sIt 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 linesval 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 concurrentlygo deeper
Knows it limits how many tasks use the service at once.
Can set it on the spec and pairs it with usesService.
Explains it's only enforced for declared consumers, isn't intra-task synchronization, and unset means unlimited.
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.