Fix Azure DevOps pipeline timeout issues with proven troubleshooting approaches and performance improvements
Troubleshooting Azure DevOps Pipeline Timeouts:
5 Real-World Fixes That Work
Your pipeline timed out. The deployment window is closing. Stakeholders are asking questions. This guide covers the five most common real-world causes — with exact error messages, diagnostic commands, YAML fixes, and step-by-step runbooks for each one. No theory. Just the fix.
First: Read the Error Message Correctly — Not All Timeouts Are the Same
Azure DevOps uses the word "timeout" loosely, and treating all timeout failures as the same problem is the fastest way to waste an hour applying the wrong fix. Before you touch the YAML or restart the agent, identify which of the five failure modes you are actually looking at. The error message — and where in the pipeline the failure appears — tells you exactly which section of this guide to jump to.
Azure DevOps Timeout Limits Reference
Before applying any fix, confirm which limit applies to your scenario. The following table shows the hard limits and defaults you are working within:
| Scenario | Default timeoutInMinutes | Maximum (YAML settable) | Hard ceiling |
|---|---|---|---|
| Microsoft-hosted agent — private repo | 60 minutes | 60 minutes (free) | 60 min on free tier. No YAML override changes this. |
| Microsoft-hosted agent — public repo | 60 minutes | 360 minutes (6 hours) | 360 min regardless of YAML setting |
| Microsoft-hosted agent — paid parallelism | 60 minutes | No fixed ceiling in YAML | Practical: set to what your job needs |
| Self-hosted agent | 60 minutes (YAML default if not set) | No platform ceiling — you control it | None — set timeoutInMinutes: 0 for unlimited |
| Azure Pipelines default (no YAML override) | 3,600 minutes (60 hours) | Settable per job | This is why long jobs sometimes appear to "hang" — they are waiting for the 60-hour default to expire |
What you see:
##[error]Finishing: Finalize Job
##[section]Finishing: Agent job 1
Result: Failed
Duration: 00:60:01
Root cause: Microsoft-hosted agents on the free tier have a hard 60-minute per-job limit for private repositories. This is a platform constraint — not a configuration issue. Setting timeoutInMinutes: 120 in your YAML does nothing on the free tier. The job terminates at exactly 60:00 regardless of what YAML says. The limit is 360 minutes (6 hours) for public repositories and has no fixed ceiling on paid parallelism plans.
How to confirm it is this problem: the job ran successfully to minute 60:00 and then terminated mid-execution. The total duration shown in the pipeline run summary is exactly 60 minutes. There is no network error, no agent crash — just the exceeded execution time message.
Confirm the exact duration in the pipeline run summary
Open the failed run → click the job → check Duration at the top of the job page. If it shows 00:60:00 or 00:60:01, it is the hard limit. If it shows a different duration, you have a different problem.
Option A: Split the job into multiple smaller jobs
The most architecturally sound fix. Break your pipeline into jobs that each complete in under 50 minutes (leave headroom). Jobs run on separate agent instances and each gets its own 60-minute window. Use dependsOn to chain them in sequence. See YAML below.
Option B: Purchase paid parallelism for the organisation
In Azure DevOps → Organization Settings → Billing → Add Microsoft-hosted parallel jobs. Cost: $40/month per parallel job (as of 2026). Paid plans have no per-job execution time ceiling. One parallel job purchase unlocks unlimited per-job duration on Microsoft-hosted agents for all pipelines in the organization.
Option C: Migrate the long-running job to a self-hosted agent
Self-hosted agents have no platform-imposed execution time ceiling. Create an Azure VM (Standard B2s is sufficient for most build workloads), install the Azure Pipelines agent, and target the job to your self-hosted pool. The job duration is then limited only by your YAML timeoutInMinutes setting — set it to 0 for unlimited.
Option D: Optimise the job to complete in under 55 minutes
Profile which tasks take the longest using the pipeline run summary timeline view. Common optimizations: enable NuGet/npm/pip caching to avoid re-downloading packages on every run, parallelize independent tasks using the matrix strategy, and remove redundant test runs or artifact publishing steps that are not needed for the specific pipeline stage.
# jobs:
# - job: BuildAndTest
# steps:
# - task: ... # 35 mins
# - task: ... # 30 mins
# - task: ... # 25 mins ← KILLS HERE at 60:00
# After: Two jobs chained with dependsOn
jobs:
- job: Build
timeoutInMinutes: 55 # Explicit — leaves 5 min headroom
pool:
vmImage: 'ubuntu-latest'
steps:
- task: ... # 35 mins
- task: ... # 15 mins — total: ~50 mins ✓
# Publish artifact so next job can pick it up
- task: PublishPipelineArtifact@1
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'drop'
- job: Test
dependsOn: Build # Waits for Build to succeed
timeoutInMinutes: 55
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
inputs: { artifact: 'drop' }
- task: ... # 25 mins — total: ~25 mins ✓
Enable the Cache@2 task to cache npm, pip, Maven, NuGet, or Gradle dependencies between runs. A cache hit on a large dependency tree saves 5–20 minutes per job — often the difference between a job that fits in 60 minutes and one that does not.
Example: - task: Cache@2 → key: '"npm" | "$(Agent.OS)" | package-lock.json' → path: $(npm_config_cache)
What you see:
running and has a healthy network connection. Anything that terminates an agent
process, starves it for CPU, or blocks its network access can cause this error.
Rebuilding the agent pool or restarting the agent service may resolve this issue.
Duration: 01:20:35
Root cause: The agent process lost communication with Azure DevOps while a job was running. The agent sends regular heartbeat messages to Azure DevOps. When those stop for longer than the heartbeat timeout period, Azure DevOps marks the agent as unresponsive and fails the job. This is a symptom — the underlying cause is what you need to fix. Common causes: the agent VM ran out of memory or disk space during the job, a long-running task consumed all CPU and starved the agent process, the network connection between the agent VM and Azure DevOps was interrupted, or the agent process was killed by an OOM killer or OS process manager.
Check agent VM resource consumption at the time of failure
SSH into the agent VM and check: free -h (memory), df -h (disk), top (CPU). If memory is near zero or disk is at 100%, the agent was resource-starved. Check dmesg | grep -i "killed" for OOM killer events.
Review the agent's internal diagnostic logs
Self-hosted agents store detailed internal logs in the _diag folder at the agent installation root (e.g., /home/azureagent/_diag/ or C:\agent\_diag\). The most recent Agent_*.log file contains the last actions the agent performed before losing contact. Look for the last successful log entry to identify exactly where execution stopped.
Fix: Memory — increase VM size or reduce job memory footprint
If OOM killed: resize the agent VM to a larger SKU (e.g., Standard_D4s_v5 instead of Standard_B2s), or split memory-intensive steps into separate jobs. For Docker builds, set --memory limits on containers to prevent them from consuming all available agent memory.
Fix: Disk — add a workspace clean step and increase agent disk
Add a workspace clean directive to the job: workspace: clean: all under the job definition. This cleans old build artifacts before each run. Also configure Docker to prune unused images: docker system prune -f as a pre-job step to prevent Docker layer accumulation from filling the disk.
Fix: Network — verify agent connectivity to Azure DevOps endpoints
The agent requires outbound HTTPS (port 443) to specific Azure DevOps endpoints. Test connectivity: curl -v https://dev.azure.com and curl -v https://vsrm.dev.azure.com from the agent VM. If these fail, the firewall or NSG is blocking agent communication. See Fix 4 for proxy/firewall configuration.
Fix: Set cancelTimeoutInMinutes to give long tasks time to report
Add cancelTimeoutInMinutes: 5 to your job definition. This gives the agent 5 minutes to complete any cleanup tasks and report final status when the job is cancelled due to timeout — giving you more diagnostic information in the logs before the run ends.
- job: Build
timeoutInMinutes: 180 # 3 hours — set to what you actually need
cancelTimeoutInMinutes: 5 # Give agent time to report before kill
pool:
name: 'Myself-hostedPool'
workspace:
clean: all # Clean workspace before every job run
steps:
- script: |
# Prune Docker to prevent disk exhaustion
docker system prune -f --volumes
df -h # Log disk state before build begins
free -h # Log memory state
displayName: 'Pre-build: clean Docker + log resources'
ls -lt /home/azureagent/_diag/Agent_*.log | head -5
# Read the last 100 lines of the most recent log
tail -100 /home/azureagent/_diag/Agent_20260101-120000-utc.log
# Check for OOM kill events
dmesg | grep -i "out of memory\|Killed process"
# Check current disk usage
df -h
# Check memory available
free -h
# Restart the agent service (Linux)
sudo systemctl restart vsts.agent.*
sudo systemctl status vsts.agent.*
What you see:
To request a free parallelism grant, please fill out the following form: https://aka.ms/azpipelines-parallelism-request
— OR —
Status: Queued
Duration: 01:23:47 ← (never moved past Queued state)
Root cause: Azure DevOps limits the number of pipeline jobs that can run simultaneously based on your organisation's parallelism quota. On the free tier, you have exactly one parallel job slot. If that slot is occupied, every other pipeline queues indefinitely. This manifests as a "timeout" when teams misread a never-started job as one that timed out during execution. New Azure DevOps organisations no longer receive a free parallelism grant automatically — Microsoft disabled automatic free grants to prevent abuse. You must either request a grant or purchase parallelism.
Confirm this is a parallelism issue — not a timeout
Open the queued pipeline run. If the status is "Queued" and the job never moved to "Running" — even after 10+ minutes — it is a parallelism problem. In Organisation Settings → Pipelines → Parallel jobs, check "Used" vs "Allocated". If Used = Allocated, every new job will queue.
Request a free parallelism grant (for legitimate projects)
Submit the Microsoft form at aka.ms/azpipelines-parallelism-request. Approval typically takes 2–3 business days. This is a one-time request and grants one free parallel job for public or private projects.
Purchase additional parallel jobs (for production teams)
Organisation Settings → Billing → Add Microsoft-hosted parallel jobs ($40/month each) or Self-hosted parallel jobs ($15/month each). Purchasing unlocks both the additional slot and removes the per-job 60-minute ceiling on Microsoft-hosted agents.
Immediate workaround: migrate to a self-hosted agent
Self-hosted agent slots are free (one parallel job per agent machine). Install an agent on any VM or machine you control and target your pipeline to the self-hosted pool. This bypasses the Microsoft-hosted parallelism limit entirely and provides unlimited parallel slots (one per agent machine).
Reduce wasted parallelism: cancel stale PR builds automatically
Old PR builds consuming parallelism slots while the PR has been updated are wasted slots. Configure pipeline triggers to automatically cancel in-progress runs when a new commit is pushed to the same PR. Add to your YAML trigger configuration: pr: autoCancel: true.
branches:
include: [main, develop]
pr:
branches:
include: ['*']
autoCancel: true # Cancel in-progress run when new commit pushed to same PR
jobs:
- job: CI
timeoutInMinutes: 45
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Build $(Build.BuildNumber)"
What you see:
##[error]Error: connect ETIMEDOUT dev.azure.com:443
##[error]HTTP 407 Proxy Authentication Required
Task: NuGetCommand — hung for 14 minutes with no output
##[error]The process '/usr/bin/dotnet' failed with exit code 1
Root cause: Self-hosted agents behind corporate firewalls or HTTP proxies fail in ways that produce no useful error messages by default. The agent connects to Azure DevOps successfully at startup (polling works), but individual tasks — NuGet restore, npm install, Docker push, Helm deploy — make their own network calls. These task-level calls do not automatically inherit the agent's proxy configuration. The task hangs waiting for a network response that never comes, then eventually hits the job timeout.
Enable verbose logging to confirm network calls are hanging
In the pipeline run, set the variable System.Debug = true (see the Verbose Logging section at the end of this guide). With verbose logging, the task log will show each network call — making it clear that a specific HTTPS call is hanging or returning a non-200 response.
Test network connectivity from the agent VM
SSH into the agent machine and test the specific endpoints each task needs: curl -v https://api.nuget.org/v3/index.json, curl -v https://registry.npmjs.org, curl -v https://dev.azure.com/YOUR-ORG. A connection refused or timeout confirms the firewall is blocking it.
Configure the agent proxy settings
If a proxy is required, configure it in the agent's .proxyconfig file (see CLI command below). This tells the agent process to route traffic through the proxy. But note: this only affects the agent heartbeat and job coordination traffic — not necessarily task-level calls.
Set proxy environment variables for task-level calls
Add the following pipeline variables so individual tasks can discover and use the proxy: http_proxy, https_proxy, no_proxy. These are standard environment variables respected by curl, npm, pip, git, Docker, and most CLI tools.
Work with your network team to allow Azure DevOps endpoints
The Azure Pipelines agent requires outbound HTTPS to a specific set of endpoints. Get your network team to allow dev.azure.com, *.dev.azure.com, *.vsassets.io, *.vsrm.visualstudio.com, and *.pkgs.visualstudio.com on port 443. The full list is in the Microsoft documentation at aka.ms/azpipelines-agents-firewall.
# Linux / macOS
./config.sh --proxyurl http://proxy.company.com:8080 \
--proxyusername proxyuser \
--proxypassword yourpassword
# Or edit the .proxyconfig file directly
echo "http://proxy.company.com:8080" > /home/azureagent/agent/.proxyconfig
# List endpoints that must be accessible from the agent VM
ENDPOINTS=(
"dev.azure.com"
"vsrm.dev.azure.com"
"vsblob.blob.core.windows.net"
"*.pkgs.visualstudio.com"
)
for ep in "${ENDPOINTS[@]}"; do
echo -n "Testing $ep: "
curl -s --max-time 5 "https://$ep" > /dev/null && echo "OK" || echo "FAILED"
done
http_proxy: "http://proxy.company.com:8080"
https_proxy: "http://proxy.company.com:8080"
no_proxy: "localhost,127.0.0.1,.internal.company.com"
jobs:
- job: Build
pool:
name: 'SelfHostedPool'
steps:
- script: |
# Verify proxy is working before main build tasks
curl -v --proxy-insecure https://dev.azure.com/$(System.TeamFoundationCollectionUri)
displayName: 'Connectivity check'
What you see:
Preparing deployment...
...
[No further output for 18 minutes]
...
##[error]The job running on agent has exceeded the maximum execution time of 60 minutes.
— In verbose logs —
##[debug]Response code: 401 Unauthorized
##[debug]Token has expired. Token expiry: 2026-03-15T00:00:00Z
Root cause: An Azure service connection uses a service principal with a client secret (or a certificate) to authenticate to the Azure subscription. Service principal secrets have an expiry date — typically 1 or 2 years. When the secret expires, deploy tasks that call the Azure Resource Manager API receive a 401 Unauthorized response. The task does not fail fast — it retries, waits, or hangs silently. The result is a task that appears to be "doing something" for many minutes before finally timing out the entire job. The timeout is a symptom. The expired credential is the actual problem.
Confirm the service connection is expired
Project Settings → Service connections → find the connection used by the failing task. Click "Edit" and then "Verify". If verification fails with an authentication error, the credential has expired. Also check the expiry date in Entra ID: search for the service principal name → Certificates & Secrets → verify the secret expiry date.
Rotate the service principal secret
In Microsoft Entra ID → App registrations → find the service principal → Certificates & secrets → New client secret → set expiry → copy the new value immediately (it only shows once). Then update the service connection in Azure DevOps: Project Settings → Service connections → Edit → update the service principal key field with the new secret → Verify and Save.
Re-run the failed pipeline to confirm the fix
After updating the service connection, re-run the failed pipeline. The deploy task should now authenticate successfully within the first 30 seconds. If it still hangs, the service connection may be pointing to the wrong subscription, or the service principal may be missing RBAC roles on the target resource group.
Long-term fix: migrate to Workload Identity Federation
Azure DevOps now supports Workload Identity Federation (OIDC) for service connections — this eliminates the need for a service principal secret entirely. The service connection federates with Azure DevOps's OIDC issuer; no secret is stored, no secret expires. Convert the service connection in Project Settings → Service connections → Edit → convert to Workload Identity Federation. Requires Azure DevOps 2022+ and subscription with Microsoft Entra integration.
Prevent future expiry: set a calendar reminder and use Key Vault rotation
Service principal secrets default to 1-2 year expiry but give no automatic notification when they expire. Set a recurring calendar reminder 60 days before expiry. For automated management, store the secret in Azure Key Vault with rotation enabled and configure a Key Vault event to trigger a Logic App or Azure Automation runbook that updates the service connection programmatically.
az ad app list --display-name "YOUR-SERVICE-CONNECTION-NAME" \
--query "[].{AppId:appId, Name:displayName}" -o table
# Check credential expiry for a specific app
az ad app credential list \
--id YOUR-APP-ID \
--query "[].{End:endDateTime, Description:customKeyIdentifier}" -o table
# Add a new client secret (valid 1 year)
az ad app credential reset \
--id YOUR-APP-ID \
--append \
--years 1 \
--display-name "ADO-Service-Connection-$(date +%Y-%m)" \
--query "{ clientId: appId, clientSecret: password }" -o json
# Copy the clientSecret value and update in Azure DevOps
# Project Settings → Service Connections → Edit → key field
Converting to Workload Identity Federation (OIDC-based) service connections eliminates service principal secrets entirely. No secret means no expiry. No expiry means no surprise pipeline failures at 2am six months after someone created a service connection and forgot to document when the secret expires. This migration takes approximately 10 minutes per service connection and is the long-term answer to Fix 5.
Prevention: YAML Timeout Defaults and Monitoring Every Pipeline Should Have
The five fixes above address failures after they happen. These controls prevent them from occurring silently and reduce the time-to-diagnosis when they do.
1. Always Set Explicit timeoutInMinutes on Every Job
The Azure Pipelines default job timeout is 3,600 minutes — 60 hours. A hung job on a self-hosted agent will wait 60 hours before failing if you have not set an explicit timeout. Every job in every pipeline should have an explicit timeoutInMinutes set to the maximum legitimate time it should take, plus a 20% buffer.
name: $(Build.DefinitionName)-$(Build.BuildNumber)
trigger:
branches: { include: [main, release/*] }
pr: { autoCancel: true } # Cancel stale PR builds to free slots
variables:
TIMEOUT_BUILD: 45 # Max legitimate build time + 20% buffer
TIMEOUT_TEST: 30
TIMEOUT_DEPLOY: 20
stages:
- stage: Build
jobs:
- job: BuildApp
timeoutInMinutes: $(TIMEOUT_BUILD)
cancelTimeoutInMinutes: 5 # Give agent time to report before kill
pool: { vmImage: 'ubuntu-latest' }
retryCountOnTaskFailure: 1 # Retry transient failures once
steps:
- # Cache dependencies to cut job time
- task: Cache@2
inputs:
key: '"npm" | "$(Agent.OS)" | package-lock.json'
path: $(npm_config_cache)
restoreKeys: 'npm | "$(Agent.OS)"'
- script: npm ci
- script: npm run build
- task: PublishPipelineArtifact@1
inputs: { targetPath: dist, artifact: drop }
- stage: Deploy
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployProd
timeoutInMinutes: $(TIMEOUT_DEPLOY)
environment: production
strategy: { runOnce: { deploy: { steps: [
- task: AzureWebApp@1
inputs:
azureSubscription: 'MyServiceConnection' # Must be verified before run
appName: my-webapp
]}}}
Enabling Verbose Diagnostic Logs in Azure Pipelines
When a pipeline fails with a timeout and the logs show nothing useful, the first step is always enabling verbose logging. Verbose mode logs every API call, every network request, every internal agent action — exposing what was happening in the seconds before the failure.
Method 1 — Add System.Debug variable to the pipeline run
In the pipeline, click "Run pipeline" → Variables → Add variable → Name: System.Debug → Value: true. This enables verbose logging for that single run without modifying your YAML. Use this for one-off diagnosis.
Method 2 — Add System.Debug to YAML variables (temporarily)
Add System.Debug: true under the variables section of your YAML. Remove it after diagnosis — verbose logs are significantly larger and slower to load.
Read the verbose logs: look for the last successful action
In the verbose task log, every line is timestamped. Find the last timestamp before the silence began. That timestamp tells you exactly which operation the agent was waiting on when it stopped responding. A gap of several minutes between log entries is the signal — identify what was called immediately before the gap.
Download raw logs for large failures
In the pipeline run → top-right menu → "Download logs". The downloaded ZIP contains raw log files for every task. The agent_diagnostic.log file inside the ZIP contains agent-level events that do not appear in the pipeline UI — particularly useful for heartbeat loss and proxy failures.
Quick Reference: Match Your Error to the Fix
Frequently Asked Questions
Related FAVRITE Articles
- The Hidden Cloud Drain: How to Find and Kill Orphaned Azure Resources Automatically
- Stop Using Connection Strings: A Step-by-Step Guide to Azure Managed Identities in 2026
- How to Fix AKS Cluster Auto-Upgrade Not Executing During Scheduled Maintenance Window
- Azure App Service Log Stream with Least Privilege: The Complete Guide