tracking
CI Minute Tracking and Monitoring
Overview
Effective cost optimization starts with understanding where your compute minutes are being consumed. GitLab provides multiple levels of tracking: namespace, project, pipeline, and job.
Usage Tracking Hierarchy
Namespace (Group/Personal)
Project A
Pipeline #123
Job: build (5 min)
Job: test (10 min)
Job: deploy (2 min)
Pipeline #124
...
Project B
...
Viewing Compute Minute Usage
For Groups (Recommended)
Navigate to: Group Settings Usage Quotas Pipelines
What You See:
- Total compute minutes used this month
- Percentage of quota consumed
- Minutes remaining
- Historical usage (current month only by default)
- Projects list sorted by CI/CD minute usage (descending)
- Includes all projects in the namespace and subgroups
Example:
Total used: 45,000 / 50,000 (90%)
Remaining: 5,000 minutes
Top Projects:
1. agent-mesh: 12,500 min (27.8%)
2. agent-router: 8,300 min (18.4%)
3. platform-agents: 6,200 min (13.8%)
...
For Personal Namespaces
Navigate to: Avatar Settings Usage Quotas
Shows the same information but for your personal projects only.
Project-Level Usage
Navigate to: Project Settings CI/CD Pipelines Usage Statistics
See compute minute consumption for a specific project over time.
Usage Calculation
How Minutes Are Computed
Job Compute Minutes = (Job Duration in Seconds / 60) Cost Factor
Pipeline Compute Minutes = Sum of all job compute minutes
Important: Jobs can run concurrently, so total compute usage can exceed the pipeline duration.
Example:
# Pipeline runs for 10 minutes end-to-end # But jobs run in parallel: test-unit: # 8 minutes test-e2e: # 10 minutes test-integration: # 6 minutes # Total compute: 8 + 10 + 6 = 24 minutes # Pipeline duration: 10 minutes (wall-clock time)
Cost Factor Impact
| Runner Type | Cost Factor | 10-Min Job Cost |
|---|---|---|
| Linux Small | 1x | 10 minutes |
| Linux Medium | 2x | 20 minutes |
| Linux Large | 4x | 40 minutes |
| Windows | 2x | 20 minutes |
| macOS | 6x | 60 minutes |
| Self-Hosted | 0x | 0 minutes |
What Counts Toward Usage
Counted:
- All jobs running on GitLab-hosted runners
- Job duration from start to finish (including setup/teardown)
- Failed jobs (wasted minutes!)
- Canceled jobs (until cancellation point)
Not Counted:
- Jobs on self-hosted runners (FREE!)
- Trigger jobs (no execution, just coordination)
- Jobs that are skipped by rules
- Pipeline setup/coordination overhead
Historical Usage Tracking
Monthly Reset
Usage resets on the 1st day of each month. All namespaces start at 0 minutes.
Viewing Past Usage
Current Limitation: GitLab UI shows only the current month by default.
Workaround - API for Historical Data:
# Get namespace usage for a specific month curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "https://gitlab.com/api/v4/namespaces/:id/ci_minutes" # For specific date range (requires Premium/Ultimate) curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "https://gitlab.com/api/v4/namespaces/:id/ci_minutes?from=2025-01-01&to=2025-01-31"
Export Usage Data with Miller
# Get current month usage by project glab api /groups/blueflyio/usage_stats | \ jq '.projects[]' | \ mlr --json stats1 -a sum -f ci_minutes -g name | \ mlr --json sort -nr ci_minutes # Track trends over time (save monthly) glab api /groups/blueflyio/usage_stats | \ jq -r '.projects[] | [.name, .ci_minutes] | @csv' >> ci-usage-$(date +%Y-%m).csv
Setting Up Usage Alerts
Built-In Email Notifications
GitLab automatically sends email alerts when:
- 75% quota reached - Warning notification
- 95% quota reached - Critical notification
- 100% quota reached - Quota exhausted notification
Recipients: Namespace owners and maintainers
Configure: Settings Notifications Usage Quotas
Custom Alert Thresholds
For Ultimate Tier:
Create custom monitoring alerts using the GitLab API:
# .gitlab-ci.yml monitor_usage: stage: .pre rules: - if: $CI_PIPELINE_SOURCE == "schedule" script: - | USAGE=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "https://gitlab.com/api/v4/namespaces/$CI_PROJECT_NAMESPACE_ID/ci_minutes" | \ jq '.minutes_used') QUOTA=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "https://gitlab.com/api/v4/namespaces/$CI_PROJECT_NAMESPACE_ID/ci_minutes" | \ jq '.monthly_minutes_limit') PERCENT=$((100 * USAGE / QUOTA)) if [ $PERCENT -gt 80 ]; then echo "WARNING: Using $PERCENT% of CI minutes ($USAGE / $QUOTA)" # Send to Slack/email/etc fi
Schedule this job to run daily or weekly.
Third-Party Monitoring
Datadog Integration:
# Monitor GitLab CI minute usage in Datadog monitors: - type: metric alert query: 'avg(last_1h):gitlab.ci.minutes.used{namespace:blueflyio} > 40000' name: 'GitLab CI Minutes High Usage' message: 'CI minute usage is above 80% of monthly quota'
Prometheus/Grafana:
# Scrape GitLab Runner metrics - job_name: 'gitlab-runner' static_configs: - targets: ['runner.example.com:9252']
Tracking by Pipeline
View Pipeline Compute Usage
Navigate to: Project CI/CD Pipelines Select Pipeline
Details Shown:
- Total pipeline duration (wall-clock time)
- Individual job durations
- Job cost factors
- Total compute minutes consumed
API Method:
# Get pipeline details including minute usage glab api /projects/:id/pipelines/:pipeline_id | \ jq '.duration, .stages[].jobs[] | {name, duration, runner}'
Identify Expensive Pipelines
# Find pipelines over 100 minutes glab api /projects/:id/pipelines | \ jq '.[] | select(.duration > 6000) | {id, ref, duration, created_at}' # Calculate cost of a specific pipeline glab api /projects/:id/pipelines/:pipeline_id/jobs | \ jq '[.[] | (.duration / 60) * (.runner.cost_factor // 1)] | add'
Tracking by Job
Job-Level Details
Navigate to: Project CI/CD Jobs Select Job
Information Available:
- Job duration (HH:MM:SS)
- Runner type and cost factor
- Compute minutes consumed
- Artifacts size
- Cache usage
Find Most Expensive Jobs
# Get all jobs from recent pipelines glab api /projects/:id/pipelines/:pipeline_id/jobs | \ jq -r '.[] | [.name, .duration, .runner.description] | @tsv' | \ sort -k2 -rn | head -20 # Calculate cost per job type glab api /projects/:id/pipelines/:pipeline_id/jobs | \ mlr --json stats1 -a mean,sum -f duration -g name | \ mlr --json put '$cost = $duration_sum / 60' | \ mlr --json sort -nr cost
Jobs That Waste Minutes
Failed Jobs:
# Find frequently failing jobs glab api /projects/:id/pipelines/:pipeline_id/jobs | \ jq '[.[] | select(.status == "failed")] | group_by(.name) | map({name: .[0].name, failures: length})'
Long-Running Jobs:
# Jobs over 30 minutes glab api /projects/:id/jobs | \ jq '.[] | select(.duration > 1800) | {name, duration, pipeline_id}'
Usage Attribution
By Team/Project Group
For Subgroups:
Each subgroup has its own usage quota allocation:
blueflyio/ (Parent Group - 50,000 min)
agent-platform/ (Subgroup A - inherits quota)
agent-mesh/
agent-router/
ossa/ (Subgroup B - inherits quota)
openstandardagents/
View subgroup usage: Group Subgroup Settings Usage Quotas
Cost Center Tagging
Tag Projects with Labels:
# .gitlab-ci.yml variables: COST_CENTER: "platform-engineering" TEAM: "agents-team"
Export and analyze:
# Generate cost report by team for project in $(glab repo list --json | jq -r '.[].path_with_namespace'); do TEAM=$(glab api /projects/$(echo $project | sed 's/\//%2F/g')/variables/TEAM 2>/dev/null | jq -r '.value // "untagged"') MINUTES=$(glab api /projects/$(echo $project | sed 's/\//%2F/g') | jq '.statistics.ci_minutes_used // 0') echo "$TEAM,$project,$MINUTES" done | mlr --csv stats1 -a sum -f 3 -g 1
Budget Alerts and Quotas
Purchasing Additional Minutes
When quota runs out:
- Cost: $10 per 1,000 additional minutes
- Purchase: Settings Usage Quotas Buy more minutes
- Validity: 1 year from purchase date
- Rollover: Purchased minutes roll over month-to-month
Per-Project Limits (Self-Managed)
For GitLab self-managed instances:
Set Project-Specific Quota:
# Rails console project = Project.find_by_full_path('group/project') project.shared_runners_minutes_limit = 5000 project.save
Quota Enforcement
When quota exhausted:
- Pipelines on shared runners stop immediately
- Jobs fail with quota exceeded error
- Self-hosted runners continue working (unaffected)
- Existing jobs complete, new jobs blocked
API Reference
Essential API Endpoints
# Namespace usage (group or personal) GET /namespaces/:id/ci_minutes # Project statistics GET /projects/:id # Returns: statistics.ci_minutes_used # Pipeline details GET /projects/:id/pipelines/:pipeline_id # Job details GET /projects/:id/jobs GET /projects/:id/jobs/:job_id
Automation Script
#!/bin/bash # ci-usage-report.sh NAMESPACE_ID="12345" ALERT_THRESHOLD=80 USAGE=$(glab api /namespaces/$NAMESPACE_ID/ci_minutes | jq '.minutes_used') QUOTA=$(glab api /namespaces/$NAMESPACE_ID/ci_minutes | jq '.monthly_minutes_limit') PERCENT=$((100 * USAGE / QUOTA)) echo "CI Minute Usage Report - $(date)" echo "================================" echo "Used: $USAGE minutes" echo "Quota: $QUOTA minutes" echo "Percentage: $PERCENT%" echo "" if [ $PERCENT -gt $ALERT_THRESHOLD ]; then echo " WARNING: Usage exceeds ${ALERT_THRESHOLD}% threshold!" echo "" # Get top projects echo "Top 10 Projects by Usage:" glab api /groups/$NAMESPACE_ID/usage_stats | \ jq -r '.projects[] | "\(.name): \(.ci_minutes) min"' | \ sort -t: -k2 -rn | head -10 fi
Monitoring Dashboards
GitLab Ultimate Analytics
Navigate to: Group Analytics CI/CD Analytics
Metrics Available:
- Pipeline success rate
- Mean time to detect (MTTD)
- Mean time to recovery (MTTR)
- Deployment frequency
- Compute minute trends (when enabled)
Custom Grafana Dashboard
{ "dashboard": { "title": "GitLab CI Minute Usage", "panels": [ { "title": "Monthly Usage Trend", "targets": [ { "expr": "gitlab_ci_minutes_used{namespace='blueflyio'}" } ] }, { "title": "Top Projects", "targets": [ { "expr": "topk(10, gitlab_ci_minutes_used_per_project)" } ] }, { "title": "Usage vs Quota", "targets": [ { "expr": "gitlab_ci_minutes_used / gitlab_ci_minutes_quota * 100" } ] } ] } }
Troubleshooting
"Usage shows 0 but pipelines ran"
Cause: Jobs ran on self-hosted runners Solution: Check runner tags. Self-hosted runners don't consume quota.
"Can't see historical usage"
Cause: UI only shows current month Solution: Use API to fetch historical data or export monthly reports
"Different numbers in different views"
Cause: Caching delays or timezone differences Solution: Wait a few minutes, refresh, or use API for real-time data
"Charged for failed jobs"
Cause: Jobs consumed runner time before failing Solution: Implement fail-fast patterns and fix failing jobs quickly
Next Steps
- Strategies - Learn how to reduce minute consumption
- Monitoring - Set up ongoing tracking and dashboards
- Checklist - Quick reference for daily checks