This episode covers how to test your application across many combinations of language versions and databases with a parallel matrix, split a large test suite across many parallel runners with the parallel keyword, and read the CI_NODE_INDEX and CI_NODE_TOTAL variables to divide the work.

In episode 9 we made pipelines flow like a DAG with needs:. Now let's attack the speed problem from another angle: how much work can run at the same time. Imagine an application that must be tested on Node.js 18, 20, and 22, alongside PostgreSQL and MySQL databases. If you copy the job definition six times, that's not just wasteful — every duplication is a potential source of unsynced bugs.
GitLab solves this with two features: parallel: matrix: to test many configuration combinations from a single job definition, and parallel: N to split one large job into N automatic runners. Both are the main weapons of teams that want fast pipelines without rewriting config. Let's break them down.
Before discussing matrices, let's understand the foundation — the parallel keyword. With parallel: 5, GitLab creates five instances of the same job running concurrently, each named job_name 1/5, job_name 2/5, and so on:
e2e_tests:
stage: test
image: node:20-alpine
parallel: 5
script:
- npm ci
- npm run test:e2eIf left as is, all five jobs run exactly the same commands — pointless. The parallel value becomes useful only when each copy does a different portion of the work. GitLab provides two predefined variables for that:
CI_NODE_INDEX — the current copy number, starting from 1.CI_NODE_TOTAL — the total number of copies.With both, you can split a test suite. A real example with Playwright, which has a built-in --shard flag:
e2e_tests:
stage: test
image: mcr.microsoft.com/playwright:v1.45
parallel: 4
script:
- npm ci
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTALEach copy runs --shard=1/4, --shard=2/4, and so on — one large suite divided evenly across four runners. Total pipeline time drops to a quarter. Modern test frameworks (Jest, Vitest, pytest-xdist, Playwright) almost all support this kind of index-based splitting, so the pattern is highly portable.
Tip
To keep shards balanced, don't just split by file order. Many frameworks offer "shard by test duration" so slow files don't pile up on one runner. The principle is the same in GitLab and other tools: total time is determined by the slowest shard, not the average.
parallel: matrix: goes one level up: instead of N identical copies, you define a matrix of value combinations, and GitLab creates one job per combination. Each key's value becomes an environment variable usable inside the script:
test_app:
stage: test
image: node:20-alpine
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
DB_ENGINE: ["postgres", "mysql"]
script:
- echo "Menguji Node.js $NODE_VERSION dengan $DB_ENGINE"
- npm test -- --env $DB_ENGINEFrom one job definition, GitLab produces six jobs (3 Node versions times 2 databases): combinations Node 18 + postgres, Node 18 + mysql, Node 20 + postgres, and so on. The key values — NODE_VERSION and DB_ENGINE — automatically become environment variables inside the job, so the script can adapt to the combination currently running. In the GitLab UI, each combination appears as a separate job labeled like test_app 2/6, and you can view each combination's status independently.
Matrices are most useful when the combination genuinely changes the job's behavior. For example: choosing the base image according to the version being tested, or choosing the database service according to the engine:
test_app:
stage: test
parallel:
matrix:
- NODE_VERSION: ["18", "20"]
DB_ENGINE: ["postgres", "mysql"]
image: node:${NODE_VERSION}-alpine
services:
- name: $DB_ENGINE:16-alpine
alias: db
script:
- npm test -- --db $DB_ENGINENotice two interesting patterns here. First, image: is filled via the NODE_VERSION variable interpolation — the Node 20 combination is automatically tested in a Node 20 container. Second, services: with name: $DB_ENGINE:16-alpine picks the database service based on the combination. All of this happens without a single if — just matrix value interpolation.
Warning
Watch out for combinatorial explosion. Two arrays of 3 values each produce 9 jobs, and adding one more array (e.g. 2 OS versions) doubles it to 18. GitLab limits the number of parallel jobs per pipeline, and your runner quota also has limits. Plan your matrix deliberately: full cross-testing is fine for CI on main, while a lean subset is enough for every regular push.
Sometimes you don't want all combinations to run — for instance a certain combination isn't supported by the app. GitLab provides include: and exclude: inside the matrix to refine the list:
test_app:
stage: test
image: node:20-alpine
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
DB_ENGINE: ["postgres", "mysql"]
exclude:
- NODE_VERSION: "22"
DB_ENGINE: "mysql"
script:
- npm test -- --env $DB_ENGINEWith exclude, the "Node 22 + mysql" combination (which, let's say, isn't supported yet) is dropped from the list — five jobs total instead of six. include works the opposite way: adding special combinations that the cartesian product doesn't produce, for instance one combination with an extra config flag. The match rules must be exact: values must equal combinations the matrix actually produces, otherwise exclude is silently ignored.
Matrix and needs (episode 9) are a powerful pair. A deploy job that needs all test results can wait using needs that mentions all combinations, or simply wait for the stage to complete:
deploy_staging:
stage: deploy
needs:
- job: test_app
artifacts: false
script:
- echo "Semua kombinasi matrix lolos, deploy"Because test_app with a matrix produces many jobs, needs: - job: test_app makes deploy_staging wait for all of those combinations to finish. This is a proper gate pattern: deploy only happens after Node 18/20/22 with postgres and mysql all pass — without rewriting a long combination list.
In this episode 10, you've mastered parallelism in GitLab CI:
parallel: N creates N copies of a job running concurrently; paired with CI_NODE_INDEX and CI_NODE_TOTAL to split test suites into shards.parallel: matrix: produces one job per value combination — e.g. NODE_VERSION times DB_ENGINE — and each value becomes an environment variable.if branch.include / exclude refine the combination list, and combining with needs creates a clean deploy gate.With matrix and parallel, a single job definition now produces dozens of concurrently running tasks — the same pipeline, far less time. Starting next episode we move to a bigger scale: splitting pipelines into separate files. In episode 11 we'll cover pipeline modularization with include: and child pipelines — how to import configuration from files, projects, templates, even remote sources, plus parent-child pipelines perfect for monorepo architecture. See you in episode 11!