How to fix fatal: Authentication failed - CI token expired during long build in Git

GitINTERMEDIATEMEDIUMUpdated Dec 6, 2025· 194 views

This error occurs when a Git authentication token expires during a long-running CI/CD build process. The token that was valid at the start of the job becomes invalid before Git operations complete, causing authentication failures mid-build.

Quick fix

Fixes fatal: Authentication failed - CI token expired during long build

# In GitHub Actions, check the job summary
# In GitLab CI, check the pipeline duration
# In Jenkins, check the build time in console output
  1. Check token expiration time vs build duration
  2. Use the native CI token instead of custom tokens
  3. Implement token refresh for long builds
  4. Split long builds into separate jobs
git — error
# In GitHub Actions, check the job summary
# In GitLab CI, check the pipeline duration
# In Jenkins, check the build time in console output
fatal: Authentication failed - CI token expired during long build

What this error means

This error indicates that the authentication token used by your CI/CD pipeline to access Git repositories has expired while the build was still running. CI/CD systems typically use time-limited tokens (Personal Access Tokens, GitHub App tokens, or OAuth tokens) that have a defined lifetime. When a build takes longer than the token's validity period, any Git operations attempted after expiration will fail with an authentication error. This is particularly common in: - Long-running builds with extensive test suites - Builds that perform Git operations at multiple stages - Pipelines with multiple sequential jobs sharing the same token - Builds that fetch additional repositories or submodules late in the process The root cause is a timing mismatch: the token's expiration time is shorter than the build duration. Most CI systems generate tokens at job start, so if your build runs for 2 hours but the token expires after 1 hour, any Git operation in the second half will fail.

Typical symptoms

  • Build fails with 'fatal: Authentication failed' partway through execution
  • Git operations succeed early in the pipeline but fail later
  • Clone works but subsequent fetch or push operations fail
  • Builds that used to pass now fail without code changes
  • Longer builds fail more frequently than shorter ones
  • Error appears inconsistently depending on build duration
  • Submodule updates fail late in the build process
  • Git LFS operations fail after main repository operations succeed

Common causes

  1. CI job duration exceeds the token's expiration time
  2. GitHub App installation tokens expire after 1 hour by default
  3. Personal Access Token is close to expiration when build starts
  4. Pipeline uses a shared token that expires during execution
  5. Build parallelism causes token refresh race conditions
  6. Token is generated once and reused across multiple sequential jobs
  7. Network delays or retries push Git operations past token expiry
  8. Nested submodule fetches extend build time beyond token validity

How to fix "fatal: Authentication failed - CI token expired during long build"

1Check token expiration time vs build duration

First, identify the mismatch between your token's lifetime and your build duration:

Check build duration:

bash
# In GitHub Actions, check the job summary
# In GitLab CI, check the pipeline duration
# In Jenkins, check the build time in console output

Token expiration times by type:
| Token Type | Default Expiration |
|------------|-------------------|
| GitHub App installation token | 1 hour |
| GitHub Actions GITHUB_TOKEN | Job duration (with refresh) |
| GitLab CI job token | Job duration |
| Personal Access Token | User-configured (often 90 days) |
| Azure DevOps PAT | User-configured |

If your build consistently takes longer than your token's expiration, you need to either reduce build time or implement token refresh.

2Use the native CI token instead of custom tokens

Most CI systems provide built-in tokens that automatically refresh or have extended validity:

GitHub Actions (GITHUB_TOKEN):

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # GITHUB_TOKEN is automatically available and refreshes
          token: ${{ secrets.GITHUB_TOKEN }}

      # For operations requiring more permissions
      - name: Push changes
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitLab CI (CI_JOB_TOKEN):

yaml
build:
  script:
    - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/group/repo.git
    # CI_JOB_TOKEN is valid for the entire job duration

Azure DevOps (System.AccessToken):

yaml
steps:
  - checkout: self
    persistCredentials: true
    # System.AccessToken is automatically managed
3Implement token refresh for long builds

For builds that exceed token lifetime, implement a refresh mechanism:

GitHub Actions with GitHub App:

yaml
jobs:
  long-build:
    runs-on: ubuntu-latest
    steps:
      - name: Generate App Token
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}

      - uses: actions/checkout@v4
        with:
          token: ${{ steps.app-token.outputs.token }}

      - name: Long running task
        run: |
          # ... your long build steps ...

      # Refresh token before late-stage Git operations
      - name: Refresh Token
        id: refresh-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}

      - name: Late Git Operations
        run: |
          git push origin main
        env:
          GITHUB_TOKEN: ${{ steps.refresh-token.outputs.token }}

Generic approach with token regeneration:

bash
#!/bin/bash
# Function to refresh token before Git operations
refresh_git_token() {
  # Call your token generation API/service
  NEW_TOKEN=$(curl -s "https://your-token-service/generate")
  git remote set-url origin "https://x-access-token:${NEW_TOKEN}@github.com/org/repo.git"
}

# Use before critical Git operations
refresh_git_token
git push origin main
4Split long builds into separate jobs

Break your pipeline into smaller jobs with fresh tokens:

GitHub Actions:

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-id: ${{ steps.upload.outputs.artifact-id }}
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: npm run build
      - uses: actions/upload-artifact@v4
        id: upload
        with:
          name: build-output
          path: dist/

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      # Fresh token for this job
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Test
        run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      # Fresh token for deployment
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./deploy.sh

Benefits:
- Each job gets a fresh token
- Parallel execution where possible
- Better failure isolation
- Clearer pipeline visibility

5Use SSH keys instead of tokens

SSH keys don't expire during builds, making them ideal for long-running pipelines:

GitHub Actions with SSH:

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup SSH Key
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Clone via SSH
        run: git clone git@github.com:org/repo.git

      - name: Long running build
        run: |
          # Hours later, SSH still works
          make build-all

      - name: Push changes
        run: |
          cd repo
          git push origin main

GitLab CI with SSH:

yaml
before_script:
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | ssh-add -
  - mkdir -p ~/.ssh
  - ssh-keyscan gitlab.com >> ~/.ssh/known_hosts

build:
  script:
    - git clone git@gitlab.com:group/repo.git
    # SSH key has no expiration

Important: Store SSH keys securely in your CI secrets and use deploy keys with minimal permissions.

6Optimize build time to stay within token limits

If possible, reduce build duration to fit within token expiration:

Caching strategies:

yaml
# GitHub Actions
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

# GitLab CI
cache:
  paths:
    - node_modules/
    - .npm/

Parallel test execution:

yaml
# GitHub Actions matrix
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npm test -- --shard=${{ matrix.shard }}/4

Skip unnecessary Git operations:

yaml
# Shallow clone
- uses: actions/checkout@v4
  with:
    fetch-depth: 1  # Only latest commit

# Don't fetch submodules if not needed
- uses: actions/checkout@v4
  with:
    submodules: false

Move Git operations early:

yaml
steps:
  # Do ALL Git operations first while token is fresh
  - uses: actions/checkout@v4
  - run: git fetch --all
  - run: git submodule update --init

  # Then do long-running non-Git tasks
  - run: npm install
  - run: npm run build
  - run: npm test
7Configure longer token expiration

If your CI system allows, extend the token lifetime:

GitHub Personal Access Tokens:
1. Go to Settings > Developer settings > Personal access tokens
2. Select or create a token
3. Set expiration to a longer period (or no expiration for CI)

GitHub App tokens:
GitHub App installation tokens are limited to 1 hour maximum. Use refresh strategies instead.

GitLab Project Access Tokens:
1. Go to Project > Settings > Access Tokens
2. Create a token with a longer expiration date
3. Use sparingly - long-lived tokens are a security risk

Azure DevOps PATs:
1. Go to User Settings > Personal Access Tokens
2. Set expiration up to 1 year
3. Consider using shorter expirations with automated rotation

Security considerations:
- Longer token lifetimes increase security risk if leaked
- Use the shortest practical expiration
- Implement token rotation for production systems
- Restrict token permissions to minimum required scopes

8Handle token expiration gracefully

Add retry logic to handle token expiration during builds:

Bash retry wrapper:

bash
#!/bin/bash
git_with_retry() {
  local max_attempts=3
  local attempt=1

  while [ $attempt -le $max_attempts ]; do
    if "$@"; then
      return 0
    fi

    echo "Git operation failed (attempt $attempt/$max_attempts)"

    # Check if it's an auth error
    if git ls-remote origin 2>&1 | grep -q "Authentication failed"; then
      echo "Authentication error detected, refreshing credentials..."
      # Add your token refresh logic here
      refresh_credentials
    fi

    attempt=$((attempt + 1))
    sleep 5
  done

  echo "Git operation failed after $max_attempts attempts"
  return 1
}

# Usage
git_with_retry git push origin main

GitHub Actions with retry action:

yaml
- uses: nick-invision/retry@v3
  with:
    timeout_minutes: 10
    max_attempts: 3
    command: |
      git push origin main

Python with retry decorator:

python
import subprocess
import time
from functools import wraps

def retry_git(max_attempts=3, delay=5):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except subprocess.CalledProcessError as e:
                    if "Authentication failed" in str(e.output):
                        print(f"Auth failed, retrying ({attempt + 1}/{max_attempts})")
                        refresh_token()  # Your refresh logic
                        time.sleep(delay)
                    else:
                        raise
            raise Exception("Max retries exceeded")
        return wrapper
    return decorator

Advanced notes

### Understanding Token Types and Lifetimes

Different CI/CD systems use different token mechanisms:

GitHub Actions GITHUB_TOKEN:
- Automatically provided and refreshed
- Scoped to the repository
- Permissions can be customized in workflow
- Generally doesn't expire during job execution

GitHub App Installation Tokens:
- Maximum lifetime: 1 hour
- Can be regenerated within workflow
- Better for organization-wide operations
- Require app installation setup

GitLab CI/CD Job Tokens:
- Valid for job duration
- Limited permissions by default
- Can access other projects with CI_JOB_TOKEN if configured

### Token Refresh Patterns

Proactive refresh:

yaml
# Refresh before expected expiration
- name: Check token age
  run: |
    TOKEN_AGE=$((($(date +%s) - $TOKEN_CREATED_AT)))
    if [ $TOKEN_AGE -gt 3000 ]; then  # 50 minutes
      echo "needs_refresh=true" >> $GITHUB_OUTPUT
    fi
  id: check

- name: Refresh if needed
  if: steps.check.outputs.needs_refresh == 'true'
  uses: actions/create-github-app-token@v1
  # ...

Reactive refresh:

bash
# Retry on auth failure with fresh token
git push || (refresh_token && git push)

### Debugging Token Issues

bash
# Check token expiration (GitHub)
curl -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/rate_limit

# Decode JWT token (if applicable)
echo $TOKEN | cut -d. -f2 | base64 -d | jq .exp

# Test authentication without side effects
git ls-remote origin HEAD

### Security Best Practices

1. Minimum privilege: Only grant tokens the permissions they need
2. Short lifetimes: Use the shortest practical expiration
3. Rotate regularly: Automated rotation prevents stale credentials
4. Audit usage: Monitor token usage in CI logs
5. Secure storage: Use CI secret management, never hardcode tokens

### Common CI Platform Specifics

GitHub Actions:
- GITHUB_TOKEN refreshes automatically
- Use actions/create-github-app-token for controlled refresh
- Matrix builds each get independent tokens

GitLab CI:
- CI_JOB_TOKEN is job-scoped and doesn't expire during job
- Use CI_DEPLOY_TOKEN for cross-project access
- Group-level tokens available for shared runners

Jenkins:
- Use Credentials plugin for token management
- Jenkins Git plugin handles many auth scenarios
- Consider Blue Ocean for better token handling

CircleCI:
- Project tokens don't expire during jobs
- Use contexts for shared secrets
- SSH keys recommended for long builds

Frequently asked questions

What does "fatal: Authentication failed - CI token expired during long build" mean?
This error indicates that the authentication token used by your CI/CD pipeline to access Git repositories has expired while the build was still running. CI/CD systems typically use time-limited tokens (Personal Access Tokens, GitHub App tokens, or OAuth tokens) that have a defined lifetime. When a build takes longer than the token's validity period, any Git operations attempted after expiration will fail with an authentication error.
How do I fix "fatal: Authentication failed - CI token expired during long build"?
This error occurs when a Git authentication token expires during a long-running CI/CD build process. The token that was valid at the start of the job becomes invalid before Git operations complete, causing authentication failures mid-build.
What causes "fatal: Authentication failed - CI token expired during long build"?
CI job duration exceeds the token's expiration time GitHub App installation tokens expire after 1 hour by default Personal Access Token is close to expiration when build starts
Is the "fatal: Authentication failed - CI token expired during long build" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading