What are stages in a GitLab CI/CD pipeline and how do they control job execution order?
Quick Answer
Stages define the sequential phases of a GitLab pipeline, such as build, test, and deploy. All jobs within the same stage run in parallel, but stages themselves execute in the order they are listed. A stage only begins after all jobs in the previous stage have completed successfully.
Detailed Answer
Think of stages like the assembly line in a car factory. The first station welds the chassis (build stage), the second station installs the engine and electronics (test stage), and the third station does the final paint and quality inspection (deploy stage). Workers at the same station can work on different parts of the car simultaneously (parallel jobs within a stage), but the car cannot move to the next station until all workers at the current station have finished. If one worker finds a defect, the entire line stops.
Stages in GitLab CI/CD are defined in the stages keyword at the top of your .gitlab-ci.yml file as an ordered list. Each job in the pipeline is assigned to exactly one stage via the stage keyword. When a pipeline is created, GitLab organizes all jobs by their stage assignment and executes them according to the stage order. Jobs within the same stage run in parallel, limited only by the number of available Runners. For example, if you have three test jobs (unit-tests, integration-tests, lint-check) all assigned to the test stage, and you have three idle Runners, all three jobs start simultaneously. The next stage (say deploy) will not begin until all three test jobs complete successfully. If any job fails, subsequent stages are skipped by default and the pipeline is marked as failed.
Internally, GitLab maintains a state machine for each pipeline. When a pipeline is created, all jobs start in the created state. As Runners become available, jobs in the first stage transition to pending and then running. When a job finishes, it moves to success or failed. GitLab's pipeline processor evaluates the overall stage status: if all jobs in the current stage are in a terminal state (success or allowed failure), it transitions jobs in the next stage to pending. The needs keyword can override this strict stage ordering by creating direct dependencies between jobs, enabling a Directed Acyclic Graph (DAG) pipeline where a job starts as soon as its specific dependencies finish, regardless of whether other jobs in the previous stage are still running. This is an optimization for pipelines where some jobs do not depend on every job in the prior stage.
In production, a well-designed stage structure for a service like payment-gateway-api typically includes five to seven stages: lint (code style checks), build (compile code and create Docker images), test (unit, integration, and end-to-end tests), security (SAST, DAST, dependency scanning), staging-deploy (deploy to a pre-production environment), acceptance (run smoke tests against staging), and production-deploy (deploy to live infrastructure). Teams often add a cleanup stage at the end to tear down temporary test environments or remove unused Docker images. The stage structure should mirror the logical phases of your delivery process, creating a clear visual representation in the GitLab pipeline view that anyone on the team can understand at a glance.
A key gotcha is that if you do not define a stages list in your .gitlab-ci.yml, GitLab uses five default stages: .pre, build, test, deploy, and .post. Jobs without an explicit stage assignment are placed in the test stage by default. This can be confusing when a job you intended to run during deployment actually runs during testing. Another common mistake is having too many sequential stages with single jobs, which eliminates parallelism and makes the pipeline unnecessarily slow. If jobs do not depend on each other, place them in the same stage so they run in parallel, or use the needs keyword for DAG pipelines.