Runners, Executors, and Job Scheduling
This domain covers which runner picks up a job, what the executor provides, and how jobs are delayed, serialised, or cancelled.
Runner selection
Runners are registered at instance, group, or project level. A job's tags: lists requirements; a runner is eligible only if it carries every tag the job lists. A job with no tags runs only on runners configured to accept untagged jobs - a common reason a job sits in pending forever. Protected runners accept jobs only from protected branches and tags. rules and only/except decide whether a job exists; tags decide which runner runs it, and both must succeed.
Executors
- shell runs on the runner host, inheriting installed tools and accumulated state - fast, but jobs can pollute each other.
- docker runs each job in a fresh container from
image:;services:starts sidecar containers (a database, Docker-in-Docker) reachable by hostname, andpull_policycontrols when images are fetched. - kubernetes creates a pod per job; docker autoscaler provisions ephemeral VMs; ssh and virtualbox cover special cases.
Because the docker executor starts a clean container, anything not baked into the image or restored from cache/artifacts is missing - the usual cause of "works on the shell runner, fails in CI".
Scheduling and concurrency
config.toml sets the runner's concurrent limit, and each runner can also carry a job limit. Pipeline schedules (cron defined in the UI or API) create pipelines with CI_PIPELINE_SOURCE == "schedule" and can inject their own variables. when: delayed with start_in: 30 minutes holds a job for a fixed wait. resource_group serialises jobs that would otherwise mutate the same environment concurrently. interruptible: true combined with auto-cancel redundant pipelines stops superseded jobs.
Worked example
deploy:
tags: [docker, production]
resource_group: production
interruptible: false
script: ./deploy.sh
Reasoning: only runners holding both docker and production are eligible, so a runner tagged only docker will never pick the job up. resource_group: production ensures two pipelines cannot deploy at once. interruptible: false protects the deploy from being auto-cancelled mid-run.
Traps
- Adding tags that no runner carries, then debugging the YAML instead of the runner list.
- Expecting shared runners to be available when the project has them disabled or its compute minutes are exhausted.
- Assuming
concurrentchanges stage ordering - stages remain sequential. - Running untrusted merge request code on a shell executor.