Skip to content

Concurrency tuning

Included free on every install, with every view of the worker pool, the parallel engine, async hooks and task status. Changing them on a running server, and the four settings that set them at start, need a license with the goroutine-engine-pro feature. See pricing.

Your instance hands background work to a shared pool of workers, runs some work in parallel, such as cache warmup and dashboard queries, and can run the hooks that follow a save in the background. Concurrency tuning shows how busy each of these is. With goroutine-engine-pro, a super admin can resize them without a restart.

PartWhat it doesDefault
Worker poolRuns the background work that features hand off.100 workers, 30 second task timeout
Parallel engineRuns a batch of tasks side by side, such as cache warmup and dashboard queries.4 at once, 30 second timeout
Async hooksRuns after-save hooks on the pool instead of inside the request.Off, 8 workers, a queue of 1024, 5 second timeout
TrackerCounts the background tasks the instance runs, by name and by feature.

Each replica has its own set, and a change made over the API applies to the replica that received it until it restarts. There are no per-tenant limits.

Open Insight > Observability and find Goroutine engine. Tracker, Worker pool, Parallel engine and Async hooks give the same numbers as the reads below, and Goroutines by owner lists the live tasks of each feature with the age of the oldest. Below them, Resize the pool, Parallel engine and Async hooks change the settings with Apply. The section notes that a change holds until the instance restarts.

The Goroutine engine section with the tracker, a worker pool of 100 with nothing waiting, the parallel engine at 4 at once, and async hooks off.

You need an admin token in TOKEN. The quickstart shows how to get one.

  1. Read the worker pool:

    Terminal window
    curl http://localhost:3001/api/admin/debug/goroutines/pool \
    -H "Authorization: Bearer $TOKEN"
    { "size": 100, "active": 0, "waiting": 0, "completed": 3, "failed": 0, "dropped": 0, "avg_latency": "127.377427ms", "task_timeout": "30s" }
  2. Read the parallel engine:

    Terminal window
    curl http://localhost:3001/api/admin/debug/goroutines/parallel \
    -H "Authorization: Bearer $TOKEN"
    { "max_concurrent": 4, "timeout": "30s" }
  3. Read the async hook queue:

    Terminal window
    curl http://localhost:3001/api/admin/debug/goroutines/async-hooks \
    -H "Authorization: Bearer $TOKEN"
    { "enabled": false, "workers": 8, "queue_size": 1024, "timeout": "5s", "queued": 0, "running": 0, "overflow": 0, "dropped": 0 }
  4. As a super admin on a license with goroutine-engine-pro, grow the pool. The answer is the new state. Without the license it is 402:

    Terminal window
    curl -X PUT http://localhost:3001/api/admin/debug/goroutines/pool \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"size": 200}'
FieldMeans
Pool active, waitingTasks running now, and tasks waiting for a free worker. A steady waiting above 0 means the pool is too small.
Pool failedTasks that ran and failed. A bigger pool does not help.
Pool droppedTasks that never started, because their caller gave up before a worker was free. Read this before you raise the size.
Async hooks overflowHooks that found the queue full and ran inside the request instead.
Status by_ownerLive tasks and the oldest one's age, per feature. A count that only grows is a leak.

Writes need a super admin and a license with goroutine-engine-pro. The pool write needs size. In the other two, a field you leave out keeps its value. Durations are strings such as 30s or 5m. Each write answers the new state and is recorded in the audit log with the values before and after.

RequestBodyAllowed values
PUT /api/admin/debug/goroutines/pool{"size": 200}size from 1 to 4096.
PUT /api/admin/debug/goroutines/parallel{"max_concurrent": 8, "timeout": "30s"}max_concurrent from 1 to 1024. timeout above 0, at most 1h.
PUT /api/admin/debug/goroutines/async-hooks{"workers": 8, "queue_size": 1024, "timeout": "5s", "enabled": true}workers from 1 to 1024. queue_size from 1 to 65536. timeout above 0, at most 10m.

Growing the pool lets waiting work start at once. Shrinking it lets running work finish. Switching async hooks on or off applies from the next hook.

A change made over the API is lost on restart, so set the matching variable under settings too when it should last.

Every view keeps working. The changes do not:

  • Each PUT answers 402 with payment_required, naming feature:goroutine-engine-pro, and nothing changes.
  • GET /api/admin/debug/goroutines/tuning answers {"licensed": false}, so a script can check before it writes.
  • The four variables under settings are ignored. The instance logs a warning naming goroutine-engine-pro and starts with the defaults: a pool of 100 workers, after-save hooks run in order inside the request, and features warm their caches and stop one at a time.

When a license lapses, the running settings stay until the next restart, and the next write answers 402.

These set the starting values. The first four need a license with goroutine-engine-pro. Without it they are ignored with a warning in the log, and the defaults apply. With it, a value that does not parse or is out of range is logged and skipped, and the others still apply.

VariableWhat it doesDefaultNeeds
GOROUTINE_ENGINE_ENABLEDWarms feature caches and stops features in parallel, instead of one at a time.falsegoroutine-engine-pro
GOROUTINE_ENGINE_POOL_SIZEWorkers in the pool, from 1 to 4096.100goroutine-engine-pro
ASYNC_HOOKS_ENABLEDRuns after-save hooks in the background from the start.falsegoroutine-engine-pro
ASYNC_HOOK_TIMEOUTHow long one async hook may run.5sgoroutine-engine-pro
DB_WARMUP_PARALLELISMHow many tasks the parallel engine runs at once. Must be positive.4Free

A DB_WARMUP_PARALLELISM that does not parse, or is not positive, stops the boot with a message naming the variable. Concurrency tuning runs on every install. If you set LYEVE_PLUGINS to choose which features start, include goroutine-engine in it. See licensing and tiers.

MethodPathWhoAnswers
GET/api/admin/debug/goroutines/statusAdmintotal, runtime_total, by_source, by_owner, max_goroutines, leak_threshold.
GET/api/admin/debug/goroutines/poolAdminsize, active, waiting, completed, failed, dropped, avg_latency, task_timeout.
GET/api/admin/debug/goroutines/parallelAdminmax_concurrent, timeout.
GET/api/admin/debug/goroutines/async-hooksAdminenabled, workers, queue_size, timeout, queued, running, overflow, dropped.
GET/api/admin/debug/goroutines/tuningAdminlicensed: whether the writes below are allowed.
PUT/api/admin/debug/goroutines/poolSuper admin, with goroutine-engine-proResize the pool.
PUT/api/admin/debug/goroutines/parallelSuper admin, with goroutine-engine-proChange the parallel engine.
PUT/api/admin/debug/goroutines/async-hooksSuper admin, with goroutine-engine-proChange async hooks.
StatusMessage
400invalid JSON, also sent for a field the route does not take.
402payment_required, naming feature:goroutine-engine-pro, on any write without the license.
409async hooks need a worker pool, and this install runs without one
422nothing to change: set max_concurrent or timeout (or workers, queue_size, timeout or enabled)
422timeout must be a duration such as 30s
422A value outside the allowed range. The message states the range.