Job Execution Order and Parallelism
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page with Pro
Job Execution Order and Parallelism in YAML Pipelines
Introduction: Why Pipeline Orchestration Matters
In the world of continuous integration and continuous delivery (CI/CD), a pipeline is much more than a list of commands executed in sequence. It is a logical representation of your software delivery lifecycle. Whether you are building a simple static website or orchestrating a complex microservices architecture, the order in which your tasks run and how they interact with each other dictates the efficiency, reliability, and speed of your development process.
When we talk about "Job Execution Order and Parallelism," we are addressing the fundamental mechanics of how your automation engine processes work. By default, many systems execute steps one after another. While this is predictable, it is often inefficient. If you have five test suites that take ten minutes each, running them sequentially will cost you nearly an hour of pipeline time. If you can run them in parallel, you reduce that time to just over ten minutes.
Understanding how to control this flow is what separates a basic pipeline script from an enterprise-grade automation strategy. This lesson will guide you through the structural components of YAML pipelines, specifically focusing on how to define dependencies, leverage parallel execution, and manage resource constraints to create high-performing delivery workflows.
The Anatomy of a Pipeline: Stages, Jobs, and Steps
Before we dive into parallelism, we must establish a clear mental model of the hierarchy within a YAML pipeline. Most modern CI/CD systems (like Azure DevOps, GitHub Actions, or GitLab CI) share a similar hierarchical structure.
- Pipeline/Workflow: The top-level container that defines the entire process.
- Stages: High-level groupings that represent major milestones, such as "Build," "Test," and "Deploy." Stages usually run sequentially, where the next stage waits for the previous one to complete successfully.
- Jobs: The units of execution within a stage. Jobs run on specific agents (virtual machines or containers). Multiple jobs within a single stage can run in parallel by default.
- Steps: The individual tasks inside a job, such as running a script, installing a dependency, or publishing an artifact. Steps within a single job always run sequentially.
Understanding the Default Flow
By default, most systems assume that if you define multiple jobs in a single stage, they should run at the same time. Conversely, if you define stages, the pipeline will wait for Stage A to finish before starting Stage B. This default behavior is designed to balance simplicity with performance. However, as your project grows, you will inevitably need to override these defaults to handle dependencies, such as ensuring a database migration job completes before the application deployment job begins.
Callout: Sequential vs. Parallel Execution Sequential execution is the process of running tasks one after another, where Task B cannot start until Task A finishes. This is ideal for tasks that depend on the output of the previous one. Parallel execution allows multiple tasks to run simultaneously on different compute resources, drastically reducing total pipeline duration. The challenge for engineers is identifying which tasks are truly independent and can safely run in parallel.
Controlling Execution Order with Dependencies
The most common requirement in pipeline design is managing dependencies. You need to ensure that your "Deploy" job does not attempt to push code to a server if the "Build" job failed.
Using dependsOn
The dependsOn keyword is the primary mechanism for controlling execution order. By explicitly defining which jobs or stages must complete before another starts, you create a directed acyclic graph (DAG) of your pipeline.
Consider the following YAML structure:
stages:
- stage: Build
jobs:
- job: CompileCode
steps:
- script: echo "Compiling..."
- stage: Test
dependsOn: Build
jobs:
- job: UnitTests
steps:
- script: echo "Running unit tests..."
- stage: Deploy
dependsOn: Test
jobs:
- job: DeployToStaging
steps:
- script: echo "Deploying..."
In this example, the Test stage will not start until the Build stage is finished. If Build fails, the Test stage is skipped entirely. This prevents the "cascading failure" problem where a pipeline continues to run despite a critical error in an earlier phase.
Complex Dependency Scenarios
Sometimes, a job might need to run regardless of whether the previous job succeeded or failed, perhaps to perform cleanup or reporting. Most CI/CD systems allow you to specify conditions like succeeded(), failed(), or always().
- job: Cleanup
dependsOn: Build
condition: always()
steps:
- script: echo "Cleaning up artifacts, even if build failed."
Note: Always be explicit with dependencies in complex pipelines. Relying on implicit order often leads to "flaky" pipelines where jobs run out of sync due to slight changes in system load or timing.
Implementing Parallelism: Strategies and Best Practices
Parallelism is your most effective tool for reducing "Time to Feedback." When a developer pushes code, they want to know if it passed tests immediately. If your test suite takes 40 minutes, they will likely switch context to another task, slowing down their overall productivity.
Job-Level Parallelism
Within a single stage, jobs run in parallel by default. If you define three jobs, the pipeline engine will request three separate agents from your pool and execute them simultaneously.
jobs:
- job: Linting
steps:
- script: npm run lint
- job: SecurityScan
steps:
- script: npm run scan
- job: UnitTests
steps:
- script: npm run test
In this scenario, Linting, SecurityScan, and UnitTests start at the same time. The total time for this stage is equal to the time taken by the longest-running job, rather than the sum of all three.
Matrix Parallelism (The "Secret Weapon")
What if you need to run the same set of tests against multiple versions of a runtime, such as Node.js 16, 18, and 20? Instead of writing three separate jobs, you can use a matrix strategy.
jobs:
- job: TestMatrix
strategy:
matrix:
node16:
node_version: 16
node18:
node_version: 18
node20:
node_version: 20
steps:
- script: echo "Testing with Node $(node_version)"
This creates three parallel jobs dynamically. If you decide to add Node 22, you simply update the matrix list, and the pipeline scales automatically. This is a best practice for cross-platform testing and environment validation.
Callout: Agent Limits and Resource Contention While parallel execution is powerful, it is limited by your available compute resources. If you define 50 parallel jobs but only have 5 build agents available, 45 jobs will sit in a queue waiting for an agent. Over-parallelizing can lead to "queue starvation," where the overhead of managing the queue exceeds the time saved by parallelism. Always monitor your agent pool saturation.
Advanced Orchestration: Strategies for Complex Pipelines
As pipelines grow, you encounter scenarios that require more than just simple dependsOn logic. You might need to gate deployments, use manual approvals, or handle multi-region deployments.
Manual Interventions and Gates
In production environments, you rarely want to deploy automatically without a human check. Most pipelines allow you to define environments with approval requirements.
- stage: DeployProduction
dependsOn: DeployStaging
jobs:
- deployment: Production
environment: 'Production-Approval-Gate'
strategy:
runOnce:
deploy:
steps:
- script: ./deploy.sh
By linking a job to an "Environment" that requires manual approval, the pipeline pauses execution until a designated user clicks "Approve." This is a critical security practice to prevent unauthorized or accidental code pushes.
Fan-In and Fan-Out Patterns
The "Fan-Out" pattern is when one job triggers multiple parallel jobs (like our matrix example). The "Fan-In" pattern is when multiple parallel jobs must all complete before a single downstream task can proceed.
- Fan-Out: Start with one build, then split into parallel unit tests, integration tests, and security scans.
- Fan-In: Collect the results of all those parallel tests and run a single "Report Generation" or "Deployment" job.
The dependsOn keyword handles this naturally. If a job lists multiple dependencies, the system waits for all of them to finish successfully before starting the dependent job.
- job: ReportGeneration
dependsOn:
- UnitTests
- IntegrationTests
- SecurityScan
steps:
- script: ./generate-summary.sh
Comparison Table: Execution Control Features
| Feature | Purpose | Best Used For |
|---|---|---|
dependsOn |
Defines sequential order | Enforcing logical flow (Build -> Test -> Deploy) |
condition |
Adds logic to execution | Handling failures, cleanup, or conditional deployments |
strategy: matrix |
Parallelizes identical tasks | Cross-version or cross-platform testing |
environment |
Adds manual gates | Production deployments requiring human sign-off |
maxParallel |
Limits concurrency | Preventing resource exhaustion on agent pools |
Common Pitfalls and How to Avoid Them
Even with a solid understanding of the mechanics, it is easy to fall into traps that make pipelines brittle or slow.
1. The "Monolithic Job" Trap
Many beginners write one giant job with 50 steps. This is a mistake. If the 49th step fails, you have no way to restart just that step. By breaking your pipeline into smaller, logical jobs (e.g., Build, Test, Lint, Package), you gain the ability to re-run only the failed portion, saving significant time.
2. Ignoring State Persistence
When you split jobs, they run on different agents. The files created by the Build job are not automatically available to the Test job. You must use "Artifacts" to publish files from one job and download them in another. Forgetting this is the most common cause of "File Not Found" errors in complex pipelines.
3. Over-Engineering Dependencies
It is tempting to create a deeply nested dependency tree. However, overly complex graphs are difficult to debug and modify. If you find your pipeline needs a 10-level deep dependency chain, it is likely that your stages or jobs are too granular. Aim for a flat, predictable structure whenever possible.
4. Hardcoding Values
Avoid hardcoding environment names, agent pool names, or versions in your pipeline. Use variables. This allows you to reuse the same pipeline structure for different branches or environments without modifying the YAML file every time.
Tip: Use a "Pipeline Template" approach for shared logic. If you find yourself copying and pasting the same set of jobs across five different repositories, move that YAML code into a central repository and reference it as a template. This ensures consistency and makes it easier to update your CI/CD standards globally.
Step-by-Step: Designing a Resilient Pipeline
To put this all into practice, let’s design a pipeline for a web application that requires a build, parallel tests, and a gated deployment.
Step 1: Define the Build Stage
Start by compiling your code and creating an artifact. This is the foundation for all subsequent steps. Use a publish step to store the binaries.
Step 2: Define the Test Stage with Parallelism
Create a stage that depends on the Build stage. Inside this stage, define three parallel jobs: Lint, UnitTests, and SecurityScan. Ensure they all download the artifact created in Step 1.
Step 3: Define the Deployment Stage with a Gate Create a final stage that depends on the Test stage. Configure this stage to point to a production environment that requires manual approval.
Step 4: Add Conditionals for Reliability
Add an always() cleanup job that runs after the Test stage to delete temporary build files from the agent, ensuring the next run starts with a clean slate.
Step 5: Review and Refine Run the pipeline and check the "Visual" view provided by your CI/CD tool. Does the DAG look logical? Are the parallel jobs actually starting at the same time? If not, check your agent pool capacity.
Best Practices for Enterprise Pipelines
- Fail Fast: Place your fastest and most critical checks (like linting or syntax checking) at the very beginning of the pipeline. If the code is broken, you want to know in seconds, not after a 10-minute build.
- Keep Jobs Atomic: Each job should have a single responsibility. A job should either build, test, or deploy. Mixing these concerns makes pipelines harder to maintain.
- Use Descriptive Names: Name your jobs and stages clearly (e.g.,
Build_Frontend,Test_Database_Migration). When looking at a failure log, you should know exactly where the problem occurred without opening the file. - Monitor Pipeline Duration: Track your pipeline execution time over time. If you see a trend of increasing duration, investigate which jobs are slowing down and determine if they can be further parallelized or optimized.
- Secure Your Pipelines: Ensure that sensitive information like API keys or deployment credentials are kept in secure variables or vaults, never hardcoded in your YAML.
Frequently Asked Questions (FAQ)
Q: Can I run a job in parallel if it depends on a previous job?
A: No. By definition, a dependency (dependsOn) forces a sequential relationship. You can only run jobs in parallel if they are independent of each other or if they depend on the same parent job.
Q: What happens if one job in a parallel set fails?
A: By default, the entire pipeline will fail once the remaining jobs complete. If you want a job to be optional, you can set continueOnError: true. This allows the pipeline to proceed even if that specific job fails.
Q: How do I know how many parallel jobs my system supports? A: This is determined by your "Parallel Jobs" quota in your CI/CD provider (e.g., Azure DevOps or GitHub Actions). You can check this in your organization's billing or settings page.
Q: Is it better to have more stages or more jobs? A: Use stages for logical milestones that require a pause or a change in environment. Use jobs for parallel execution units. Too many stages create unnecessary waiting periods; too many jobs can overwhelm your agent pool.
Key Takeaways
- Hierarchy Matters: Understanding the relationship between stages, jobs, and steps is the foundation of effective pipeline architecture.
- Sequential vs. Parallel: Use sequential execution for dependencies (Build -> Test) and parallel execution for independent tasks (Linting + Testing) to optimize for speed.
- The Power of Matrix: Use matrix strategies to run the same logic across multiple environments or versions, which is far more maintainable than duplicating jobs.
- Dependency Management: Utilize
dependsOnto build a clean, reliable execution graph, but avoid over-complicating it with unnecessary chains. - Gating Deployments: Always use environment gates for production deployments to ensure human oversight and compliance.
- Resource Awareness: Parallelism is not infinite. Balance your desire for speed against the constraints of your available agent pool to avoid queue congestion.
- Maintenance: Treat your pipeline code as software. Use templates, avoid hardcoding, and keep jobs atomic to ensure your automation strategy scales as your organization grows.
By mastering these concepts, you shift from simply "writing scripts" to "engineering delivery systems." The goal is to provide the fastest, most reliable feedback loop possible to your development team, enabling them to ship high-quality software with confidence. Take the time to audit your current pipelines—identify where you are running things sequentially that could be parallel, and look for places where you can add better dependency controls to prevent failures. Your future self, and your team, will thank you.
Continue the course
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons