Webhook icon
Schedule icon
Request icon
Script icon
If icon
Loop icon
Log icon
Create icon
SlackIncomingWebhook icon

Keep GitLab runner pools balanced so pipelines stop queuing

Report GitLab CI runner pool balance per tag when a pipeline waits too long, then suggest idle runner reassignments, open a scaling issue and ping Slack.

Categories
Infrastructure

Pipelines queue when the runner pool for a tag is saturated while other runners sit idle, and nobody notices until the merge queue stalls. This flow fires from a webhook the moment a pipeline has been pending past its threshold, reads the project's online runners and its pending and running jobs from the GitLab API, and merges them into a per-tag busy versus idle picture with a queue depth and a verdict. When the busiest tag crosses the imbalance ratio or more than five jobs are waiting, it fans out a reassignment suggestion for every idle runner, opens a GitHub scaling issue and pings Slack; a balanced pool is logged and exits quietly. The verdict, online runner count, busy ratio and queue depth come back as typed outputs.

How it works

  1. pipeline_pending (Webhook) receives the pipeline event posted by the relay once a job has waited longer than the alerting threshold. hourly_rebalance_sweep is a disabled Schedule kept as a fallback sweep until someone switches it on.
  2. fetch_online_runners (io.kestra.plugin.core.http.Request) calls GET /projects/{path}/runners?status=online with the GITLAB_TOKEN secret and retries on transient failures.
  3. fetch_queued_jobs (io.kestra.plugin.core.http.Request) calls GET /projects/{path}/jobs?scope[]=pending&scope[]=running for the same project, which yields both the queue depth and the runner ids that are busy right now.
  4. compute_balance (io.kestra.plugin.scripts.python.Script) merges the two payloads. It splits online runners into busy and idle by the runner id attached to each running job, divides busy by online for every tag, keeps the highest per-tag busy ratio, and prints one ::{"outputs": ...}:: line carrying verdict, online_runners, busy_ratio, idle_runners and queue_depth.
  5. balance_gate (io.kestra.plugin.core.flow.If) opens when busy_ratio is greater than inputs.imbalance_ratio or queue_depth is greater than 5. Inside it, suggest_reassignment (io.kestra.plugin.core.flow.Loop) logs one suggestion per idle runner, open_scaling_issue (io.kestra.plugin.github.issues.Create) files the scaling request, and notify_slack posts to the build channel. When the gate stays closed, log_balanced records the healthy pool.

What you get

  • verdict (STRING): SCALE_UP when the busiest tag is over the ratio or more than five jobs are queued, BALANCED when the pool can absorb the work, NO_RUNNERS when nothing is online, NO_DATA when the API answered with something unparseable.
  • online_runners (INT): runners the project reports as online for the check.
  • busy_ratio (FLOAT): highest busy ratio across the pool's tags, or the whole-pool ratio when runners carry no tags.
  • queue_depth (INT): jobs waiting in pending right now.
  • idle_runners (ARRAY): online runners with nothing assigned, each with its id, description and tags, which is exactly what the loop iterates.

Who it's for

  • Platform and CI teams running shared GitLab runner fleets who see pipelines queue while some runners sit idle.
  • Infrastructure owners who want a scaling request filed automatically instead of discovering the backlog in a standup.

Why orchestrate this with Kestra

  • Two API reads, a merge step and a branch are one versioned file instead of a cron script nobody owns, and the disabled Schedule is the fallback sweep ready to be switched on.
  • The gate, the per-runner loop and the escalations are explicit in the graph, so the threshold that triggers scaling is reviewable rather than buried in code.
  • Failures escalate through the errors handler to Slack with the execution url, so an expired token or an unreachable API is never silent.
  • Outputs are typed and machine readable, so an autoscaler or a dashboard can consume verdict and queue_depth directly.

Prerequisites

  • A Kestra instance with the GitHub and Slack plugins installed.
  • A GitLab project with runners assigned to tags and a relay that posts pipeline events to the webhook once a job has been pending past your threshold.

Secrets

  • GITLAB_TOKEN: GitLab personal or project access token that can read online runners and job scopes.
  • GITLAB_WEBHOOK_KEY: key the webhook trigger expects from the relay that posts the pipeline event.
  • GITHUB_TOKEN: GitHub token that can create the scaling issue in the ops backlog repository.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used for the rebalance ping and for failure alerts.

Quick start

  1. Add the four secrets and import the flow into your namespace.
  2. Point the relay or GitLab webhook rule at the trigger URL, so it posts only once a pipeline has been pending longer than your threshold, or execute manually with project_path.
  3. Read verdict, online_runners, busy_ratio and queue_depth in the outputs while the loop, the issue and the Slack task handle the escalation.

How to extend

  • Lower imbalance_ratio to escalate earlier, or raise it to tolerate a busier tag before suggesting a scale out.
  • Add a third input for the queue threshold and swap the hard-coded 5 in the gate condition for it when different projects need different tolerances.
  • Point repository at one backlog repository per GitLab group by templating it from project_path.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.