GitLab CI’s matrix and parallel jobs feature lets you run a single job definition across multiple configurations simultaneously, drastically cutting down on repetitive job definitions and speeding up your pipelines.
Let’s see it in action. Imagine you need to test your application against different Ruby versions and operating systems. Instead of writing three separate jobs, one for each combination, you can use a matrix:
test_app:
stage: test
parallel:
matrix:
- RUBY_VERSION: "3.0"
OS: "ubuntu-latest"
- RUBY_VERSION: "3.1"
OS: "ubuntu-latest"
- RUBY_VERSION: "3.0"
OS: "macos-latest"
script:
- echo "Running tests with Ruby $RUBY_VERSION on $OS"
- bundle install
- bundle exec rspec
When this pipeline runs, GitLab CI will spin up three distinct test_app jobs. Each job will have its own RUBY_VERSION and OS environment variables set according to the matrix configuration. You can see the distinct jobs in the pipeline view, each executing the same script but with different underlying environments.
The core problem this solves is combinatorial explosion in CI configuration. Without matrices, testing across multiple Ruby versions, Python versions, database backends, or browser types quickly leads to dozens, if not hundreds, of nearly identical job definitions. This is error-prone, hard to maintain, and bloats your .gitlab-ci.yml file. A matrix collapses these into a single, concise definition.
Internally, GitLab CI generates the individual jobs from the matrix definition at pipeline creation time. Each generated job is a fully independent execution, inheriting the base configuration of the job it was derived from, but with the specific matrix variables injected into its environment. You can even use these variables in other parts of your job configuration, like image or services.
The primary levers you control are the parallel keyword and its matrix sub-key. Inside matrix, you define a list of maps. Each map represents a single job instance, where the keys are the names of the environment variables you want to set (e.g., RUBY_VERSION, OS, NODE_ENV), and the values are the specific settings for that instance. You can also specify a max_parallel number to limit how many matrix jobs run concurrently if you have a very large matrix and want to control resource usage.
What many users miss is that the parallel keyword can also take a simple integer to run a job multiple times identically, which is different from a matrix. A matrix is for variation, while a simple integer is for redundancy or parallel execution of the exact same task. For example, parallel: 4 would run the job four times with no variable differences, useful for load testing or distributing identical tasks.
The next concept you’ll likely encounter is how to define more complex matrices using include and needs to manage dependencies between matrix jobs.