Your weekly dose of actionable cloud wisdom to start the week right
The Problem
Your Azure DevOps pipelines take 45 minutes to deploy a simple web app, developers are waiting hours for feedback on pull requests, and your monthly Azure DevOps bill keeps growing whilst productivity plummets. Meanwhile, other teams ship features daily with sub-10-minute deployments, and you’re wondering where you went wrong.
The Solution
Optimize Azure DevOps pipelines with proven techniques that dramatically reduce build times, cut costs, and improve developer productivity. Most slow pipelines suffer from poor caching, inefficient task ordering, and resource waste that can be fixed with strategic optimizations.
Essential Pipeline Optimization Techniques:
1. Intelligent Caching Strategy
# azure-pipelines.yml - Optimized caching configuration
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotNetFramework: 'net6.0'
dotNetVersion: '6.0.x'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: Build
displayName: 'Build Application'
steps:
# Cache NuGet packages
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json,!**/bin/**'
restoreKeys: |
nuget | "$(Agent.OS)"
nuget
path: $(NUGET_PACKAGES)
displayName: 'Cache NuGet packages'
# Cache npm dependencies
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | **/package-lock.json,!**/node_modules/**'
restoreKeys: |
npm | "$(Agent.OS)"
npm
path: $(npm_config_cache)
displayName: 'Cache npm dependencies'
# Cache Docker layers
- task: Cache@2
inputs:
key: 'docker | "$(Agent.OS)" | Dockerfile'
restoreKeys: |
docker | "$(Agent.OS)"
docker
path: /tmp/.buildx-cache
displayName: 'Cache Docker layers'
- task: UseDotNet@2
inputs:
version: $(dotNetVersion)
includePreviewVersions: false
displayName: 'Setup .NET Core'
# Restore packages (will use cache if available)
- task: DotNetCoreCLI@2
inputs:
command: 'restore'
projects: '**/*.csproj'
feedsToUse: 'select'
displayName: 'Restore NuGet packages'
2. Parallel Job Execution
# Parallel testing strategy
- stage: Test
displayName: 'Testing Stage'
dependsOn: Build
jobs:
# Unit Tests (fast)
- job: UnitTests
displayName: 'Unit Tests'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: '**/*UnitTests/*.csproj'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage" --logger trx --results-directory $(Agent.TempDirectory)'
displayName: 'Run Unit Tests'
# Integration Tests (slower, run in parallel)
- job: IntegrationTests
displayName: 'Integration Tests'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: '**/*IntegrationTests/*.csproj'
arguments: '--configuration $(buildConfiguration) --logger trx --results-directory $(Agent.TempDirectory)'
displayName: 'Run Integration Tests'
# Security Scanning (parallel)
- job: SecurityScan
displayName: 'Security Scanning'
steps:
- task: WhiteSource Bolt@20
inputs:
cwd: '$(System.DefaultWorkingDirectory)'
displayName: 'Run WhiteSource Security Scan'
# Code Quality (parallel)
- job: CodeQuality
displayName: 'Code Quality Analysis'
steps:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud Connection'
organization: 'your-org'
scannerMode: 'MSBuild'
projectKey: 'your-project-key'
projectName: 'Your Project'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
3. Conditional Execution and Smart Triggers
# Only run expensive tasks when necessary
- stage: Deploy
displayName: 'Deployment'
dependsOn:
- Build
- Test
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToStaging
displayName: 'Deploy to Staging'
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
# Only build Docker image if code changed
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$changedFiles = git diff HEAD~1 --name-only
$hasCodeChanges = $changedFiles | Where-Object { $_ -match '\.(cs|js|ts|json|csproj)$' }
if ($hasCodeChanges) {
Write-Host "##vso[task.setvariable variable=shouldBuildImage;isOutput=true]true"
Write-Host "Code changes detected, will build new image"
} else {
Write-Host "##vso[task.setvariable variable=shouldBuildImage;isOutput=true]false"
Write-Host "No code changes, skipping image build"
}
name: 'checkChanges'
displayName: 'Check if rebuild needed'
# Conditional Docker build
- task: Docker@2
condition: eq(variables['checkChanges.shouldBuildImage'], 'true')
inputs:
containerRegistry: 'Docker Hub'
repository: 'your-app'
command: 'buildAndPush'
Dockerfile: '**/Dockerfile'
tags: |
$(Build.BuildId)
latest
displayName: 'Build and Push Docker Image'
4. Optimize Build Agents and Resource Usage
# Use appropriate agent pools for different workloads
variables:
# Use self-hosted agents for heavy workloads to save costs
heavyWorkloadPool: 'Self-Hosted-Large'
lightWorkloadPool: 'Azure Pipelines'
stages:
- stage: QuickValidation
displayName: 'Quick Validation'
pool:
vmImage: 'ubuntu-latest' # Hosted agents for quick tasks
jobs:
- job: LintAndFormat
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
- script: |
npm install
npm run lint
npm run format:check
displayName: 'Lint and Format Check'
- stage: HeavyProcessing
displayName: 'Heavy Processing'
pool:
name: $(heavyWorkloadPool) # Self-hosted for resource-intensive tasks
jobs:
- job: BuildAndTest
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration Release'
5. Advanced Caching with Azure Artifacts
# Use Azure Artifacts for dependency caching
- task: NuGetAuthenticate@0
displayName: 'NuGet Authenticate'
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/**/packages.lock.json'
restoreKeys: |
nuget | "$(Agent.OS)"
path: '$(Pipeline.Workspace)/.nuget/packages'
displayName: 'Cache NuGet packages'
# Custom script to restore from Azure Artifacts if cache miss
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$cacheHit = $env:CACHE_RESTORED
if ($cacheHit -ne "true") {
Write-Host "Cache miss - restoring from Azure Artifacts"
dotnet restore --source "https://pkgs.dev.azure.com/yourorg/_packaging/yourfeed/nuget/v3/index.json"
} else {
Write-Host "Cache hit - using cached packages"
}
displayName: 'Smart Package Restore'
Cost Optimization Strategies
6. Pipeline Cost Monitoring
# Add cost tracking to pipelines
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$startTime = Get-Date $env:SYSTEM_JOBSTARTED
$endTime = Get-Date
$duration = ($endTime - $startTime).TotalMinutes
# Calculate approximate cost (Microsoft-hosted agents cost ~£0.006/minute)
$estimatedCost = [math]::Round($duration * 0.006, 3)
Write-Host "##vso[task.logissue type=warning]Pipeline duration: $([math]::Round($duration, 2)) minutes"
Write-Host "##vso[task.logissue type=warning]Estimated cost: £$estimatedCost"
# Log to Azure Monitor for tracking
$body = @{
pipeline = $env:BUILD_DEFINITIONNAME
duration = $duration
cost = $estimatedCost
buildId = $env:BUILD_BUILDID
} | ConvertTo-Json
# Send to monitoring endpoint
try {
Invoke-RestMethod -Uri "https://your-monitoring-endpoint.com/pipeline-costs" -Method Post -Body $body -ContentType "application/json"
} catch {
Write-Host "Failed to log cost data: $_"
}
displayName: 'Track Pipeline Costs'
condition: always()
7. Self-Hosted Agent Optimization
# Self-hosted agent setup script for cost optimization
#!/bin/bash
# Install Docker for efficient builds
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
# Install build tools
sudo apt-get update
sudo apt-get install -y git curl wget unzip
# Install .NET SDK
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y dotnet-sdk-6.0
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Configure agent for optimal performance
echo "Setting up agent configuration..."
mkdir -p /opt/azure-agent
cd /opt/azure-agent
# Download and configure agent
wget https://vstsagentpackage.azureedge.net/agent/2.213.2/vsts-agent-linux-x64-2.213.2.tar.gz
tar zxvf vsts-agent-linux-x64-2.213.2.tar.gz
# Configure with minimal capabilities for cost efficiency
sudo ./bin/installdependencies.sh
./config.sh --unattended \
--url https://dev.azure.com/yourorg \
--auth pat \
--token YOUR_PAT_TOKEN \
--pool "Self-Hosted-Optimized" \
--agent "ubuntu-agent-$(hostname)" \
--work /opt/azure-agent/_work
# Set up as service
sudo ./svc.sh install
sudo ./svc.sh start
Performance Monitoring Dashboard
# Add performance metrics to pipelines
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Agent.TempDirectory)/performance-metrics.json'
ArtifactName: 'performance-metrics'
displayName: 'Publish Performance Metrics'
condition: always()
# Custom task to generate metrics
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$metrics = @{
buildTime = $((Get-Date) - (Get-Date $env:SYSTEM_JOBSTARTED)).TotalMinutes
agentType = $env:AGENT_NAME
queueTime = $((Get-Date $env:SYSTEM_JOBSTARTED) - (Get-Date $env:BUILD_QUEUEDTIME)).TotalMinutes
cacheHits = $env:CACHE_HIT_COUNT
testCount = $env:TEST_COUNT
artifactSize = (Get-ChildItem -Path "$(Build.ArtifactStagingDirectory)" -Recurse | Measure-Object -Property Length -Sum).Sum
}
$metrics | ConvertTo-Json | Out-File "$(Agent.TempDirectory)/performance-metrics.json"
Write-Host "Performance Summary:"
Write-Host "Build Time: $([math]::Round($metrics.buildTime, 2)) minutes"
Write-Host "Queue Time: $([math]::Round($metrics.queueTime, 2)) minutes"
Write-Host "Cache Hits: $($metrics.cacheHits)"
displayName: 'Generate Performance Metrics'
Why It Matters
- Developer Productivity: Faster feedback loops = happier, more productive teams
- Cost Control: Optimized pipelines can reduce Azure DevOps costs by 60%+
- Release Velocity: Ship features faster with reliable, quick deployments
- Quality: Faster pipelines encourage more frequent testing and integration
Try This Week
- Measure current performance – Time your slowest pipeline end-to-end
- Implement basic caching – Add NuGet and npm package caching
- Parallelize tests – Split unit and integration tests into separate jobs
- Set up cost monitoring – Track pipeline duration and estimated costs
Quick Pipeline Audit Script
# PowerShell script to audit pipeline performance
param(
[string]$Organization,
[string]$Project,
[string]$PAT
)
$headers = @{
Authorization = "Basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$PAT")))"
}
$buildsUri = "https://dev.azure.com/$Organization/$Project/_apis/build/builds?api-version=6.0&`$top=100"
$builds = Invoke-RestMethod -Uri $buildsUri -Headers $headers
$performanceData = $builds.value | ForEach-Object {
$duration = if ($_.finishTime -and $_.startTime) {
([datetime]$_.finishTime - [datetime]$_.startTime).TotalMinutes
} else { 0 }
[PSCustomObject]@{
BuildId = $_.id
Definition = $_.definition.name
Duration = [math]::Round($duration, 2)
Status = $_.status
QueueTime = ([datetime]$_.startTime - [datetime]$_.queueTime).TotalMinutes
Result = $_.result
}
} | Sort-Object Duration -Descending
Write-Host "Top 10 Slowest Builds:"
$performanceData | Select-Object -First 10 | Format-Table -AutoSize
$avgDuration = ($performanceData | Measure-Object -Property Duration -Average).Average
Write-Host "Average build duration: $([math]::Round($avgDuration, 2)) minutes"
$estimatedMonthlyCost = $avgDuration * 0.006 * ($builds.value.Count * 4) # Rough monthly estimate
Write-Host "Estimated monthly cost: £$([math]::Round($estimatedMonthlyCost, 2))"
Common Pipeline Performance Killers
- No caching: Downloading dependencies on every build
- Sequential execution: Running tasks that could be parallel
- Oversized agents: Using expensive agents for simple tasks
- Inefficient Docker builds: Not using multi-stage builds or layer caching
- No conditional logic: Running unnecessary tasks on every commit
Advanced Optimization Techniques
- Matrix builds: Test multiple configurations in parallel
- Template reuse: Standardize common pipeline patterns
- Artifact promotion: Build once, deploy many times
- Progressive deployment: Deploy to subsets of infrastructure first
Pro Tip: Use Azure DevOps Analytics to identify your slowest pipeline stages. Often 80% of the time is spent in just 1-2 tasks that can be dramatically optimized with caching or parallelization.
Achieved dramatic pipeline speed improvements at your organisation? Share your optimization wins – real performance gains make the most inspiring Monday tips!








