1. Introduction

A GitLab runner timeout kills your job before it finishes and typically logs: ERROR: Job failed: execution took longer than 1h0m0s seconds. The job doesn't fail because of a bug — it fails because it ran out of time. But the underlying cause could be a genuinely slow job, a stuck process, or a misconfigured timeout that's too low for what you're doing.

This guide explains GitLab's three-level timeout hierarchy, how to tune each level, and more importantly, how to make your jobs faster so you don't need to raise timeouts as a workaround.

2. How GitLab CI Timeouts Work

GitLab CI has three timeout levels, applied in order from most to least restrictive:

3. Common Causes

4. Step-by-Step Fix

Step 1: Identify which timeout fired and which step is slow

# The error message tells you the timeout value that fired:
# "execution took longer than 1h0m0s seconds" → 60 min = default project timeout
# "execution took longer than 3600 seconds"  → same
# "execution took longer than 2h0m0s seconds" → custom 2h project timeout

# To find which step is slow, add timing to your jobs:
build:
  script:
    - date && echo "Starting build..."
    - npm ci
    - date && echo "Build complete."
    - npm run build
    - date && echo "Build artifact ready."

# Or use the built-in GitLab timestamps on each log line

Step 2: Increase project CI/CD timeout (if the job legitimately needs more time)

# In GitLab UI:
# Project → Settings → CI/CD → General pipelines → Timeout
# Change from 1h to 2h or 3h as needed

# Then set a job-level timeout in .gitlab-ci.yml:
build:
  timeout: 90 minutes
  script:
    - npm ci
    - npm run build

# Or for a specific deployment job that might take longer:
deploy-production:
  timeout: 2 hours
  script:
    - ./deploy.sh

Step 3: Fix slow dependency installation with caching

# Cache node_modules between pipeline runs
build:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
    policy: pull-push
  script:
    - npm ci --prefer-offline  # uses cache; fails if package not cached

# Cache Python packages
test:
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  cache:
    key: "$CI_JOB_IMAGE"
    paths:
      - .cache/pip/
  script:
    - pip install --cache-dir .cache/pip -r requirements.txt

Step 4: Fix slow Docker builds with layer caching

# Pull the previous image to use as cache source
build-image:
  script:
    # Pull existing image for cache (ignore failure if image doesn't exist yet)
    - docker pull $CI_REGISTRY_IMAGE:latest || true
    # Build using the pulled image as cache
    - docker build
        --cache-from $CI_REGISTRY_IMAGE:latest
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        --tag $CI_REGISTRY_IMAGE:latest
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest

Step 5: Fix jobs hanging due to missing timeout on commands

If a job appears to hang (shows no log output for many minutes), a command is waiting indefinitely. See also Fix Jenkins Pipeline Stuck for the same pattern in Jenkins.

# Always add --timeout to kubectl commands
kubectl rollout status deployment/my-app --timeout=5m
kubectl wait --for=condition=Ready pods -l app=my-app --timeout=120s

# Add timeouts to curl calls
curl --connect-timeout 10 --max-time 30 https://api.example.com

# Add timeouts to AWS CLI calls
aws --cli-read-timeout 30 --cli-connect-timeout 10   ecs wait services-stable --cluster my-cluster --services my-service

Step 6: Parallelise slow test suites

# GitLab CI parallel jobs
test:
  parallel: 4
  script:
    # Use CI_NODE_INDEX and CI_NODE_TOTAL to split tests
    - pytest tests/ --split-by $CI_NODE_TOTAL --split-index $CI_NODE_INDEX

# Or use matrix for different test groups
test:
  parallel:
    matrix:
      - TEST_SUITE: [unit, integration, e2e]
  script:
    - pytest tests/$TEST_SUITE

5. Verification Steps

# After fixing, check the pipeline duration in GitLab:
# CI/CD → Pipelines → click the pipeline → check each job duration

# Confirm the timeout is now correct relative to expected duration
# A job that normally takes 8 minutes shouldn't have a 60-minute timeout
# Set it to ~15-20 minutes to catch actual hangs without being too tight

6. Common Mistakes

7. Prevention Tips

8. FAQ

Why does my job time out on shared GitLab.com runners but not on self-hosted?

Shared GitLab.com runners have a 1-hour maximum timeout that cannot be exceeded regardless of project settings. Self-hosted runners have no runner-level timeout by default (unless configured in config.toml). For jobs that genuinely need more than 1 hour, you need a self-hosted runner.

The job log shows it's stuck on one command for 40 minutes. How do I debug?

This is a hanging command, not a slow one. Common causes: a command waiting for user input (add -y or --non-interactive flags), a network call with no timeout (add --max-time), or a subprocess that never exits. Add timeout 300 before the command to kill it after 5 minutes if you're unsure: timeout 300 your-command.

9. Summary

ScenarioFix
Job genuinely needs more timeIncrease project timeout → set job timeout in .gitlab-ci.yml
npm/pip install slowAdd cache: block with files: key and paths
Docker build slowUse --cache-from with previously pushed image
Job appears to hangAdd --timeout to kubectl/curl/aws commands
Tests take too longUse parallel: matrix to split tests across jobs

Explore More in This Category

Explore more in this category: CI/CD guides. Browse all DevOps Compass articles or jump to: Kubernetes, AWS, CI/CD, Containers, Monitoring, Networking.