CI Sizer
Resource sizing, energy estimation, and carbon footprint tracking for CI/CD runners.
Overview
CI Sizer is a two-binary Go application that monitors CI/CD runner resource usage and provides right-sizing recommendations, energy consumption estimates, and carbon footprint tracking. It consists of a collector (runs alongside runners, collects metrics from /proc) and a receiver (aggregates data, serves a web dashboard, and provides a REST API). The two components communicate via REST — the collector pushes a run summary to the receiver on shutdown.
CI Sizer reads /proc directly with zero instrumentation — no agent installation or code changes are required in CI jobs.
Supported CI Providers
| Provider | Injection Mechanism | ci_provider value |
|---|
| Forgejo Actions | GARM runner lifecycle | forgejo |
| GitHub Actions | GARM runner lifecycle | github |
| GitLab CI | MutatingAdmissionWebhook | gitlab |
The collector works identically across all providers — it reads /proc and pushes to the receiver. The injection mechanism (how the collector gets into the CI pod) differs per provider.
Key Features
- Multi-provider support — supports Forgejo Actions, GitHub Actions, and GitLab CI via provider-specific injection mechanisms
- Resource monitoring — collects CPU and memory metrics at configurable intervals via
/proc/stat and /proc/<PID>/status - Sizing recommendations — computes Kubernetes resource requests and limits from historical data, with configurable percentiles, buffers, and floors
- Confidence-gated sizing — adapts recommendation aggressiveness through three phases (unknown → learning → confident) based on available data
- OOM detection — detects out-of-memory events via cgroup v2
memory.events and applies exponential backoff recovery - Commit status notifications — posts OOM alerts to Forgejo, GitHub, or GitLab commit status APIs
- Staircase memory buffer — applies decreasing headroom as observed memory grows (20% below 1 GiB, 10% for 1–4 GiB, 5% above 4 GiB)
- Energy estimation — models per-run energy consumption using the Teads SPECpower curve and Cloud Carbon Footprint linear model
- Carbon footprint — calculates gCO2eq per run using real-time FfE hourly emission factors for the German electricity mix, with static and fallback tiers
- Web dashboard — hierarchical drill-down from overview to individual run details, with charts, compare functionality, and keyboard navigation
- OIDC authentication — supports direct OIDC login (Dex, Keycloak, Entra ID) or API gateway JWT forwarding
- Scoped push tokens — HMAC-SHA256 tokens scoped to org/repo/workflow/job; a compromised token cannot read data
- GARM integration — automated runner sizing via WebSocket lifecycle events (Forgejo/GitHub)
- GitLab webhook integration — MutatingAdmissionWebhook for GitLab Runner Kubernetes executor pods
- Container-aware grouping — maps processes to containers via cgroup paths
Architecture
The collector runs as a sidecar in CI pods with shared PID namespace. It samples /proc on a configurable interval, groups processes by container via cgroup paths, and pushes a run summary to the receiver on shutdown (SIGINT/SIGTERM).
The receiver stores metric summaries in SQLite, exposes query and sizing APIs, and serves a web UI at /ui. Internally it is decomposed into focused subpackages: auth/ (OIDC, gateway JWT, middleware), store/ (SQLite persistence), sizing/ (algorithm and overview aggregation), reporting/ (dashboard KPIs and aggregation), garm/ (GARM WebSocket client), pushtoken/ (HMAC token generation), and web/ (embedded static assets and HTML templates).
┌─────────────────────────────────────────────┐ ┌──────────────────────────┐
│ CI/CD Pod (shared PID namespace) │ │ Receiver Service │
│ │ │ │
│ ┌───────────┐ ┌────────┐ ┌───────────┐ │ │ POST /api/v1/metrics │
│ │ collector │ │ runner │ │ sidecar │ │ │ │ │
│ │ │ │ │ │ │ │ push │ ▼ │
│ │ reads │ │ │ │ │ │──────▶│ ┌────────────┐ │
│ │ /proc for │ │ │ │ │ │ │ │ SQLite │ │
│ │ all PIDs │ │ │ │ │ │ │ └────────────┘ │
│ └───────────┘ └────────┘ └───────────┘ │ │ │ │
│ │ │ ▼ │
└─────────────────────────────────────────────┘ │ GET /api/v1/sizing/... │
│ GET /ui │
└──────────────────────────┘
Getting Started
- Review the Configuration reference for all collector and receiver flags
- Deploy the receiver as a central service and the collector as a sidecar in your CI pods
- Generate scoped push tokens for each workflow/job combination
- Access the Web Dashboard at
/ui to explore metrics and sizing recommendations
For Kubernetes deployment examples, see the Configuration page.
Repository
Documentation
| Guide | Description |
|---|
| Configuration | All collector and receiver flags, environment variables, deployment examples |
| Web Dashboard | Using the web UI for resource analysis and sizing recommendations |
| Sizing Algorithm | Algorithm steps, buffers, floors, overrides, enforcement modes |
| OOM Detection | Confidence-gated sizing, OOM recovery, commit status notifications |
| Energy Estimation | Power models, carbon sources, TDP database, and academic references |
| GitLab Integration | MutatingAdmissionWebhook setup for GitLab CI |
| KPI Benchmark | Benchmark methodology and resource optimization results |
| API Reference | All endpoints, authentication, request/response examples |
1 - Configuration
Configuration reference for the CI Sizer collector and receiver binaries.
Collector Configuration
The collector runs alongside CI workloads, reads /proc, and pushes a run summary to the receiver on shutdown.
Collector Flags
| Flag | Environment Variable | Description | Default |
|---|
--interval | — | Collection interval (e.g., 5s, 1m) | 5s |
--proc-path | — | Path to proc filesystem | /proc |
--log-level | — | Log level: debug, info, warn, error | info |
--log-format | — | Output format: json, text | json |
--top | — | Number of top processes to include | 5 |
--push-endpoint | — | HTTP endpoint to push metrics to | — |
--push-token | COLLECTOR_PUSH_TOKEN | Bearer token for push endpoint auth | — |
--hardware-profile | RUNNER_HARDWARE_PROFILE | Hardware profile: JSON, preset name, or empty for auto-detect | auto-detect |
--carbon-provider | RUNNER_CARBON_PROVIDER | Carbon intensity provider: energy-charts (default, full 3-tier: Energy Charts → FfE → static), ffe (legacy alias, same chain), static (static table only) | energy-charts |
--carbon-zone | RUNNER_CARBON_ZONE | Carbon intensity zone | DE |
--pue | RUNNER_PUE | Power Usage Effectiveness multiplier | 1.3 |
Carbon Zone
The carbon zone determines which electricity grid is used for carbon intensity estimation.
| Flag | Env | Default | Description |
|---|
--carbon-zone | RUNNER_CARBON_ZONE | DE | ISO 3166-1 alpha-2 country code for the electricity grid zone |
Supported zones:
| Zone | Country | Typical CI (gCO₂eq/kWh) | Notes |
|---|
AT | Austria | ~67 | Hydro-dominated |
BE | Belgium | ~179 | Gas + nuclear mix |
CH | Switzerland | ~42 | Hydro + nuclear |
DE | Germany | ~258 | Mixed (coal/gas/wind/solar) |
DK | Denmark | ~88 | Wind-heavy |
ES | Spain | ~94 | Solar + wind + gas |
FI | Finland | ~59 | Nuclear + hydro + biomass |
FR | France | ~24 | Nuclear-dominated |
IT | Italy | ~206 | Gas-heavy |
NL | Netherlands | ~369 | Gas-dominated |
NO | Norway | ~28 | Hydro-dominated |
PL | Poland | ~505 | Coal-dominated |
SE | Sweden | ~33 | Hydro + nuclear |
Example:
# French grid (nuclear-dominated, very low CI)
./collector --carbon-zone FR
# Or via environment variable
RUNNER_CARBON_ZONE=PL ./collector
Provider chain per zone:
- DE: Energy Charts → FfE blob projection → static (seasonal/weekday)
- All others: Energy Charts → static (seasonal/weekday table with 192 values)
FfE projection data is only available for Germany. For all other zones, the static fallback provides a seasonal/weekday/hourly profile (192 values) when Energy Charts is unavailable.
CI Context Environment Variables
These environment variables identify the CI run context. They are typically set automatically by GitHub Actions / Forgejo Actions.
| Variable | Description | Example |
|---|
GITHUB_REPOSITORY_OWNER | Organization name | my-org |
GITHUB_REPOSITORY | Full repository path | my-org/my-repo |
GITHUB_WORKFLOW | Workflow filename | ci.yml |
GITHUB_JOB | Job name | build |
GITHUB_RUN_ID | Unique run identifier | run-123 |
CGROUP_PROCESS_MAP | JSON: process name to container name | {"node":"runner"} |
CGROUP_LIMITS | JSON: per-container CPU/memory limits | See below |
{
"runner": { "cpu": "2", "memory": "1Gi" },
"sidecar": { "cpu": "500m", "memory": "256Mi" }
}
CPU supports Kubernetes notation ("2" = 2 cores, "500m" = 0.5 cores). Memory supports Ki, Mi, Gi, Ti (binary) or K, M, G, T (decimal).
Note: The collector reads GITHUB_REPOSITORY (e.g., my-org/my-repo) and automatically strips the organization prefix before pushing — the payload’s repository field contains only my-repo. When generating push tokens via POST /api/v1/token, the repository field must use the short name (without the org prefix).
Receiver Configuration
The receiver stores metric summaries, serves the web UI, and provides the sizing and query APIs.
Receiver Flags
| Flag | Environment Variable | Description | Default |
|---|
--addr | — | HTTP listen address | :8080 |
--db | — | SQLite database path | metrics.db |
--read-token | RECEIVER_READ_TOKEN | Pre-shared token for read/admin endpoints | — |
--hmac-key | RECEIVER_HMAC_KEY | Secret key for push token generation/validation | — |
--token-ttl | — | Time-to-live for push tokens | 2h |
--auth-mode | RECEIVER_AUTH_MODE | Authentication mode: none, oidc, gateway | auto-detect |
--cpu-sizing-mode | RECEIVER_CPU_SIZING_MODE | CPU sizing mode: observe or enforce | observe |
--memory-qos | RECEIVER_MEMORY_QOS | Memory QoS class: guaranteed or burstable | guaranteed |
--log-level | RECEIVER_LOG_LEVEL | Log level: debug, info, warn, error | info |
OIDC / Authentication Flags
| Flag | Environment Variable | Description | Default |
|---|
--oidc-issuer | RECEIVER_OIDC_ISSUER | OIDC issuer URL | — |
--oidc-client-id | RECEIVER_OIDC_CLIENT_ID | OIDC client ID | — |
--oidc-client-secret | RECEIVER_OIDC_CLIENT_SECRET | OIDC client secret | — |
--oidc-redirect-uri | RECEIVER_OIDC_REDIRECT_URI | OIDC redirect URI | — |
--session-ttl | — | Session cookie TTL | 12h |
--session-signing-key | RECEIVER_SESSION_SIGNING_KEY | Hex-encoded 32-byte session signing key | auto-generate |
--cookie-secure | RECEIVER_COOKIE_SECURE | Set Secure flag on auth cookies; disable for plain HTTP | true |
--allowed-org | RECEIVER_ALLOWED_ORG | Allowed organization for OIDC login | — |
--logout-url | RECEIVER_LOGOUT_URL | External logout URL for gateway mode | — |
JWT Claim Mapping
| Flag | Environment Variable | Description | Default |
|---|
--claim-sub | RECEIVER_CLAIM_SUB | JWT claim for user ID | sub |
--claim-name | RECEIVER_CLAIM_NAME | JWT claim for display name | name |
--claim-email | RECEIVER_CLAIM_EMAIL | JWT claim for email | email |
--claim-groups | RECEIVER_CLAIM_GROUPS | JWT claim for groups array | groups |
--claim-org | RECEIVER_CLAIM_ORG | JWT claim for organization | org |
--org-from-groups | RECEIVER_ORG_FROM_GROUPS | Org extraction from groups: first, match, none | first |
GARM Integration Flags
| Flag | Environment Variable | Description | Default |
|---|
--garm-url | GARM_URL | GARM base URL for WebSocket event enrichment | — |
--garm-user | GARM_USER | GARM username for JWT authentication | — |
--garm-password | GARM_PASSWORD | GARM password for JWT authentication | — |
--garm-cache-ttl | GARM_CACHE_TTL | TTL for pending GARM event cache | 60s |
OOM Detection & Notification Flags
| Flag | Environment Variable | Description | Default |
|---|
--max-memory | RECEIVER_MAX_MEMORY | Node ceiling for memory (overrides auto-detection from /proc/meminfo) | auto (90% node RAM) |
--max-cpu | RECEIVER_MAX_CPU | Node ceiling for CPU | auto |
--notify-enabled | RECEIVER_NOTIFY_ENABLED | Enable commit status notifications on OOM | false |
--notify-base-url | RECEIVER_NOTIFY_BASE_URL | Forge base URL (auto-detected if unset) | — |
--notify-token | RECEIVER_NOTIFY_TOKEN | API token for forge commit status API | — |
Multi-Provider Flags
| Flag | Environment Variable | Description | Default |
|---|
--ci-provider | CI_PROVIDER | CI provider: forgejo, github, gitlab | forgejo |
GitLab-Specific Variables
These variables are relevant when CI_PROVIDER=gitlab:
| Variable | Description | Default |
|---|
CI_SIZER_RUNNER_NAME | Override runner name (defaults to pod name for GitLab) | pod name |
CGROUP_STRATEGY | Cgroup mapping strategy: default or exclusion | default |
Set CGROUP_STRATEGY=exclusion for GitLab pods where the build container process name is unpredictable. See GitLab Integration for details.
Authentication Modes
CI Sizer supports three authentication modes, configured via --auth-mode:
| Mode | Description |
|---|
none | Token-only authentication. All protected endpoints accept the Bearer read token. |
oidc | Direct OIDC login via Dex, Keycloak, or Entra ID. Most endpoints require an OIDC session cookie; sizing endpoints also accept a Bearer read token for programmatic access. |
gateway | External API gateway (e.g., APISIX) handles authentication and forwards JWTs. All protected endpoints accept the gateway JWT or Bearer read token. |
When --auth-mode is not set, the receiver auto-detects: if --oidc-client-id is provided, it defaults to oidc; otherwise it defaults to none.
Kubernetes Deployment
Receiver Deployment
docker build -f Dockerfile --target receiver -t ci-sizer-receiver:local .
kubectl create namespace ci-sizer
kubectl -n ci-sizer create secret generic receiver-secrets \
--from-literal=read-token=my-secret-token \
--from-literal=hmac-key=my-hmac-key
apiVersion: apps/v1
kind: Deployment
metadata:
name: receiver
spec:
replicas: 1
selector:
matchLabels:
app: receiver
template:
metadata:
labels:
app: receiver
spec:
containers:
- name: receiver
image: ci-sizer-receiver:local
ports:
- containerPort: 8080
env:
- name: RECEIVER_READ_TOKEN
valueFrom:
secretKeyRef:
name: receiver-secrets
key: read-token
- name: RECEIVER_HMAC_KEY
valueFrom:
secretKeyRef:
name: receiver-secrets
key: hmac-key
args: ["--addr=:8080", "--db=/data/metrics.db"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
For persistent storage, replace the emptyDir volume with a PersistentVolumeClaim.
For plain HTTP deployments (local dev, port-forward), set RECEIVER_COOKIE_SECURE=false to prevent OIDC login failures caused by browsers rejecting Secure cookies over HTTP.
Collector Sidecar
The collector runs as a sidecar in CI pods with shareProcessNamespace: true. Generate a push token first, then deploy:
apiVersion: v1
kind: Pod
metadata:
name: ci-run-test
spec:
shareProcessNamespace: true
restartPolicy: Never
containers:
- name: runner
image: busybox:latest
command: ["/bin/sh", "-c", "while true; do dd if=/dev/zero of=/dev/null bs=1M count=100 2>/dev/null; sleep 1; done"]
resources:
limits:
cpu: "500m"
memory: "256Mi"
- name: collector
image: ci-sizer-collector:local
args: ["--interval=2s", "--top=5", "--push-endpoint=http://receiver.ci-sizer.svc.cluster.local/api/v1/metrics"]
env:
- name: COLLECTOR_PUSH_TOKEN
value: "<PUSH_TOKEN>"
- name: GITHUB_REPOSITORY_OWNER
value: "my-org"
- name: GITHUB_REPOSITORY
value: "my-org/my-repo"
- name: GITHUB_WORKFLOW
value: "ci.yml"
- name: GITHUB_JOB
value: "build"
- name: GITHUB_RUN_ID
value: "test-run-001"
resources:
limits:
cpu: "100m"
memory: "64Mi"
2 - Web Dashboard
Using the CI Sizer web dashboard for resource analysis and sizing recommendations.
Overview
The receiver serves an embedded web UI at /ui for exploring runner metrics, sizing recommendations, energy consumption, and carbon footprint data. The dashboard is server-rendered with vanilla JavaScript — no build step or external dependencies are required.
Navigation
The dashboard uses a hierarchical drill-down model with card-based navigation:
- Overview — global KPI summary with per-organization breakdown
- Organization — per-repository summaries within an org
- Repository — per-workflow summaries within a repo
- Workflow/Job — charts showing resource usage, sizing, and energy data across runs
- Run Details — detailed metrics for a single CI run, including sizing recommendations and energy impact
Each level displays entities as clickable cards. Click a card (or press Enter/Space when focused) to drill down to the next level. Use the Backspace key to navigate up one level.
Keyboard Shortcuts
| Key | Action |
|---|
? | Show keyboard shortcuts help |
/ | Focus entity search |
Backspace | Navigate up one level |
Esc | Close modal |
C | Toggle compare basket |
O | Open compare view (when basket has items) |
Cmd+D / Ctrl+D | Toggle dark mode |
Enter / Space | Activate focused card |
Left / Right arrows | Navigate chart data points |
Enter (on chart point) | View details for selected data point |
Charts
At the workflow/job level, the dashboard displays interactive charts for:
- CPU usage — peak and average CPU cores per run
- Memory usage — peak and average memory per run
- Duration — run duration over time
- Success/failure — pass/fail statistics
- Energy consumption — estimated energy (Wh) per run
- Carbon footprint — estimated CO2 emissions (gCO2eq) per run
Charts support a time range selector to filter the displayed period. The x-axis can be toggled between run ID order and chronological time order.
Clicking a data point in any chart navigates to the run details view for that specific execution.
Run Details
The run details view shows comprehensive information for a single CI execution:
- Per-container CPU and memory metrics (peak, average, percentiles)
- Top CPU and memory consuming processes
- Sizing recommendations for each container (request and limit values)
- Energy consumption estimate with methodology and confidence level
- Carbon footprint estimate with carbon intensity source
Compare Feature
The compare feature allows side-by-side comparison of multiple entities (organizations, repositories, workflows, or jobs):
- Press
C to open the compare basket - Add entities to the basket from any navigation level
- Press
O or click the compare button to view a side-by-side comparison - Compare view shows KPIs, resource usage trends, and sizing recommendations across selected entities
Entity Search
Press / to focus the entity search field. Type to filter the visible cards by name. The search works at any navigation level — overview, org, repo, or workflow/job.
3 - Sizing Algorithm
How CI Sizer calculates resource sizing recommendations for runners.
Overview
CI Sizer analyses historical resource usage to recommend right-sized Kubernetes resource requests and limits for each container in a CI pod. The goal is to find the smallest allocation that safely completes the job — reducing waste without causing failures.
Methodology
The sizer computes recommendations by aggregating the N most recent clean (non-OOM) runs for a given workflow/job combination. The aggressiveness of the recommendation depends on the current confidence phase.
Confidence Phases
Every workflow/job progresses through three confidence phases as clean samples accumulate:
| Phase | Clean Samples | Behaviour |
|---|
| unknown | 0 | Returns bootstrap default: 4Gi memory, 500m CPU |
| learning | 1–2 | Applies 3× headroom above observed peak (conservative) |
| confident | ≥3 | Full algorithm with tight staircase buffer |
In the confident phase, the full algorithm below applies:
- Collect the N most recent runs (configurable via
?runs= query parameter, 1–100) - Per container, across runs:
- CPU request — take the selected percentile (default: p95) of each run’s CPU usage, then take the maximum across runs
- Memory request — take the peak memory of each run, then take the maximum across runs
- Apply buffers to add headroom above observed values
- Apply floor values to ensure minimum viable allocations
- Apply a memory ceiling — no single container can exceed the total pod memory observed across all runs (plus buffer)
- Round limits to clean values: CPU rounds up to the nearest 0.5 cores; memory rounds up to the next power of 2 in MiB
For full details on confidence phases and OOM recovery, see OOM Detection.
Query Parameters
| Parameter | Default | Description |
|---|
runs | 5 | Number of recent runs to analyse (1–100) |
buffer | 20 | CPU headroom percentage (memory uses the staircase below) |
cpu_percentile | p95 | CPU stat to use: peak, p99, p95, p75, p50, avg |
Thresholds and Floors
Every container receives a minimum viable allocation even if it was completely idle in all observed runs:
| Resource | Request Floor | Limit Floor |
|---|
| CPU | 10m | 500m |
| Memory | 32Mi | 128Mi |
Request and limit floors are intentionally asymmetric: a low request allows efficient scheduling bin-packing, while a higher limit prevents OOM kills or severe throttling if a previously-idle container becomes active.
Staircase Buffer
CPU uses a flat configurable buffer (default: 20%). Memory uses a staircase buffer — larger allocations are inherently more stable and over-provisioning them wastes more cluster resources:
| Observed Peak Memory | Buffer |
|---|
| < 1 GiB | 20% |
| 1 – 4 GiB | 10% |
| > 4 GiB | 5% |
CPU vs Memory Enforcement
Kubernetes treats CPU and memory differently, and the sizer reflects this:
- CPU is compressible — exceeding the limit causes throttling, not failure. The job continues, just slower.
- Memory is incompressible — exceeding the limit triggers an OOM kill. The job fails immediately.
Memory limits are therefore always enforced. CPU enforcement is opt-in via --cpu-sizing-mode:
| Mode | Description |
|---|
observe (default) | Compute CPU recommendations and report them, but mark enforced: false. The provider uses its own defaults. |
enforce | Apply CPU recommendations as Kubernetes requests/limits (enforced: true). |
Memory QoS
The --memory-qos flag controls the memory QoS class:
| Mode | Description |
|---|
guaranteed (default) | Memory request equals memory limit (Guaranteed QoS class). Prevents overcommit. |
burstable | Memory request is less than limit (Burstable QoS class). Allows burst above the request. |
Sizing Overrides
Operators can pin CPU and/or memory values at any scope instead of relying on the algorithm. Overrides are useful for known-heavy jobs, cost caps, or bootstrapping new workflows before enough historical data exists.
Scope Hierarchy
Overrides resolve with most-specific wins:
job > workflow > repo > org
Fields left null in an override are inherited from the next parent scope (or the algorithm). This means you can override only memory at the org level and let CPU continue to be computed from data.
Override API
| Method | Path | Description |
|---|
GET | /api/v1/sizing/overrides | List all overrides |
PUT | /api/v1/sizing/overrides/{org} | Upsert org-level override |
PUT | /api/v1/sizing/overrides/{org}/{repo} | Upsert repo-level override |
PUT | /api/v1/sizing/overrides/{org}/{repo}/{workflow} | Upsert workflow-level override |
PUT | /api/v1/sizing/overrides/{org}/{repo}/{workflow}/{job} | Upsert job-level override |
DELETE | Same paths as PUT | Remove override at that scope |
When an override is active, the sizing response includes override_scope in the meta block indicating which level matched (job, workflow, repo, org). When no override matched, the value is "global".
OOM-Aware Sizing
When OOM events are detected (via cgroup v2 memory.events or the 95%-of-limit heuristic), the sizer applies special handling:
- OOM-suspect samples are excluded from the clean sample count — they do not advance the confidence phase
- Exponential backoff on consecutive OOMs:
limit × 2^consecutiveOOMs - Node ceiling cap — backoff is bounded by the node ceiling (90% of node RAM or
--max-memory)
This ensures the sizer recovers gracefully from memory exhaustion without unbounded growth. For full details, see OOM Detection.
For the full sizing API response format, see the API Reference.
4 - OOM Detection & Confidence-Gated Sizing
How CI Sizer detects out-of-memory events and adapts sizing recommendations through confidence phases.
Overview
CI Sizer v0.7.0 introduces confidence-gated sizing — a system that adapts recommendation aggressiveness based on how much data is available for a given workflow/job. Combined with OOM detection, the sizer can automatically recover from memory exhaustion events by applying exponential backoff and notifying the source forge via commit status.
Confidence Phases
Every workflow/job combination progresses through three confidence phases as the sizer accumulates clean (non-OOM) samples:
| Phase | Condition | Behaviour |
|---|
| unknown | 0 clean samples | Returns a bootstrap default of 4Gi memory. API responds with HTTP 200 and meta.confidence_phase == "unknown". |
| learning | 1–2 clean samples | Applies 3× headroom above observed peak. Conservative to avoid OOMs while data is sparse. |
| confident | ≥3 clean samples | Uses the tight staircase buffer (20%/10%/5%). Full algorithm precision. |
Client Note
The bootstrap phase (0 samples) now returns HTTP 200 instead of 404. Clients should check meta.confidence_phase to distinguish bootstrap defaults from data-driven recommendations.OOM Detection
Cgroup v2 Detection
The collector sidecar reads the cgroup v2 memory.events file and monitors the oom_kill counter. When the counter increments during a run, the sample is marked as an OOM event.
Source: internal/cgroup/oom.go
Heuristic Detection
For environments where the oom_kill counter is not available (e.g., cgroup v1), the sizer applies a heuristic: if the observed peak memory reaches ≥95% of the configured limit, the sample is marked as OOM-suspect. OOM-suspect samples are excluded from the clean sample count used for confidence phase progression.
Exponential Backoff
When consecutive OOMs are detected for a workflow/job, the sizer applies exponential backoff to the memory limit:
new_limit = current_limit × 2^consecutiveOOMs
The backoff is capped at the node ceiling to prevent unbounded growth.
Node Ceiling
The maximum memory allocation is bounded by the node ceiling, which is determined by:
- Auto-detection — reads
/proc/meminfo and uses 90% of total node RAM - Manual override — configurable via
--max-memory flag
Similarly, --max-cpu caps the maximum CPU allocation.
Commit Status Notifications
When an OOM event is detected, the receiver posts a commit status notification to the source forge, alerting developers that their CI run was killed due to memory exhaustion.
Forgejo / GitHub
POST /api/v1/repos/{owner}/{repo}/statuses/{sha}
GitLab
POST /api/v4/projects/{id}/statuses/{sha}
Authentication is via the PRIVATE-TOKEN header for GitLab or Bearer token for Forgejo/GitHub.
Configuration
| Flag | Environment Variable | Description | Default |
|---|
--notify-enabled | RECEIVER_NOTIFY_ENABLED | Enable commit status notifications | true |
--notify-base-url | RECEIVER_NOTIFY_BASE_URL | Forge base URL (auto-detected from push metadata if unset) | — |
--notify-token | RECEIVER_NOTIFY_TOKEN | API token for forge commit status API | — |
In most deployments, only --notify-token is required. The base URL and node ceiling are auto-detected.
Source: internal/receiver/notify/notify.go
Web UI Indicators
The web dashboard surfaces OOM information through several visual elements:
- Confidence badges — displayed per workflow/job showing the current phase (unknown, learning, confident)
- OOM banners — warning banners on affected workflow/job pages
- Red markers on charts — individual OOM’d runs are highlighted with red markers on the timeline chart
Sizing Response
When OOM detection is active, the sizing API response includes additional fields in the meta block:
{
"meta": {
"confidence_phase": "learning",
"clean_samples": 3,
"consecutive_ooms": 1,
"node_ceiling_memory": "28Gi",
"node_ceiling_cpu": "14"
}
}
Source Files
| File | Purpose |
|---|
internal/cgroup/oom.go | Cgroup v2 OOM detection via memory.events |
internal/receiver/sizing/confidence.go | Confidence phase logic and phase transitions |
internal/receiver/notify/notify.go | Commit status notification dispatch |
5 - Energy Estimation
Methodology and sources for CI Sizer’s energy consumption and carbon footprint estimates.
Overview
CI Sizer estimates the energy consumption and carbon footprint of CI/CD runner executions using established industry models. CPU utilization data collected by the collector sidecar (from /proc/stat) is combined with hardware power characteristics and grid carbon intensity to produce per-run energy scores.
These are statistical estimates, not real power measurements. They are suitable for trend analysis, cross-run comparison, and sustainability reporting.
Power Estimation Models
Teads SPECpower Curve (TDP-based)
When the hardware’s Thermal Design Power (TDP) is known and no per-vCPU min/max watt bounds are set, power is estimated using a 4-point piecewise linear interpolation derived from SPECpower benchmark data.
| CPU Utilization | Power Coefficient (x TDP) |
|---|
| 0% | 0.12 |
| 10% | 0.32 |
| 50% | 0.75 |
| 100% | 1.02 |
Power(u) = TDP x interpolate(u, [0, 10, 50, 100], [0.12, 0.32, 0.75, 1.02])
Between anchor points, values are linearly interpolated. The 1.02 coefficient at 100% accounts for turbo-boost overshoot above nominal TDP.
Source: Benjamin Davy, Teads Engineering (2021), standardized by the Green Software Foundation Impact Framework.
References:
CCF Linear Model (vCPU-based)
When only the vCPU count is known (or when per-vCPU min/max watt bounds are available), the Cloud Carbon Footprint linear interpolation model is used.
Power = vCPUs x (MinWatts + u x (MaxWatts - MinWatts))
Default coefficients (AWS average from SPECpower_ssj2008 benchmarks):
| Parameter | Value | Meaning |
|---|
MinWatts | 0.74 W/vCPU | Idle power per vCPU |
MaxWatts | 3.50 W/vCPU | Max-load power per vCPU |
When a specific CPU is auto-detected, TDP-derived bounds replace the defaults:
MinWatts = TDP x 0.12 / vCPUs (idle fraction from Teads curve)
MaxWatts = TDP x 1.02 / vCPUs (max fraction from Teads curve)
Reference: Cloud Carbon Footprint Methodology
Model Selection
| Condition | Model Used |
|---|
| Profile has per-vCPU min/max watts (both > 0) | CCF Linear |
| Profile has TDP only (no min/max watts) | Teads Curve |
In practice, auto-detected CPUs derive min/max watts from TDP, so the CCF linear model is used for both generic and auto-detected profiles. The Teads curve is only used when a user provides a raw TDP-only hardware profile.
Energy Calculation
Energy_raw (kWh) = Power (W) x Duration (s) / 3600 / 1000
Energy_adjusted (kWh) = Energy_raw x PUE
Power Usage Effectiveness (PUE)
A PUE of 1.3 means the datacenter uses 30% more energy than the IT equipment alone. This is a conservative middle ground:
| Context | Typical PUE |
|---|
| Hyperscalers (Google, AWS, Azure) | 1.10–1.18 |
| New datacenter builds (Uptime 2024) | ~1.3 |
| Industry average | 1.56 |
Provider-specific PUEs (from Cloud Carbon Footprint):
| Provider | PUE |
|---|
| AWS | 1.135 |
| GCP | 1.1 |
| Azure | 1.125 |
References:
Carbon Intensity
Carbon emissions are calculated as:
Carbon (gCO2eq) = Energy (kWh) x CarbonIntensity (gCO2eq/kWh)
Carbon intensity is resolved through a 3-tier fallback chain. Each tier is tried in order; the first successful response wins. The methodology string always reflects which tier actually supplied the data.
Data Quality Comparison
The tiers differ fundamentally in what they measure — not just in precision:
| Energy Charts (Tier 1) | FfE Projection (Tier 2) | Static Table (Tier 3) |
|---|
| Data type | Real MW from actual power plants | Modeled scenario projection | Derived seasonal averages |
| Updates | Every 15 minutes | Static per projection year | Never (compiled into binary) |
| Accuracy | Actual grid state right now | Weather-year-2012 estimate | Seasonal/hourly average |
| Zones | 13 EU countries | DE only | 13 EU countries |
| Availability | Sometimes delayed or unavailable | Always available | Always available |
| Reflects today’s weather | ✅ Yes — real wind/solar/demand | ❌ No — same values every year for the same hour | ❌ No — averaged over months |
Tier 1: Energy Charts Real-Time (default)
| Property | Value |
|---|
| Source | Fraunhofer Institute for Solar Energy Systems (Fraunhofer ISE) |
| API | https://api.energy-charts.info/public_power?country={cc} |
| Data | Real-time actual generation — MW output from real power plants operating right now |
| Resolution | 15-minute intervals, updated continuously |
| Authentication | None required |
| Cache | In-memory, 15-minute TTL (matching data resolution) |
| Methodology string | energy-charts |
| Supported zones | DE, AT, FR, NL, PL, DK, CH, ES, IT, BE, SE, NO, FI |
Direct carbon intensity calculation: Carbon intensity is calculated directly from the generation mix using IPCC AR5 lifecycle emission factors per fuel type: grid_intensity = Σ(fuel_MW × emission_factor) / Σ(generation_MW). Each 15-minute interval’s generation data is fetched from the /public_power endpoint. For each production type with a known emission factor, the MW output is multiplied by the factor; the weighted sum is divided by total generation to yield gCO₂eq/kWh. Negative values (storage consumption) and non-generation keys (Load, Battery Consumption, etc.) are excluded.
IPCC AR5 lifecycle emission factors used by CI Sizer:
| Fuel Type (Energy Charts name) | gCO₂eq/kWh | Source |
|---|
| Fossil peat | 1100 | IPCC AR5 |
| Fossil brown coal / lignite | 1054 | IPCC AR5 |
| Fossil hard coal | 888 | IPCC AR5 |
| Fossil coal-derived gas | 850 | IPCC AR5 |
| Fossil oil | 733 | IPCC AR5 |
| Fossil gas | 410 | IPCC AR5 |
| Others | 400 | Conservative estimate |
| Waste | 330 | IPCC AR5 (mixed waste) |
| Biomass | 230 | IPCC AR5 |
| Solar | 45 | IPCC AR5 |
| Geothermal | 38 | IPCC AR5 |
| Other renewables | 30 | IPCC AR5 |
| Hydro Run-of-River | 24 | IPCC AR5 |
| Hydro water reservoir | 24 | IPCC AR5 |
| Hydro pumped storage | 24 | IPCC AR5 |
| Nuclear | 12 | IPCC AR5 |
| Wind offshore | 12 | IPCC AR5 |
| Wind onshore | 11 | IPCC AR5 |
For example, when the grid runs on 5000 MW lignite and 5000 MW gas: (5000×1054 + 5000×410) / 10000 = 732 gCO₂eq/kWh.
This approach calculates intensity directly from the actual fuel dispatch, providing more accurate values than the previous simplified formula.
Reference: Fraunhofer ISE — Energy Charts
Tier 2: FfE Projection Data (first fallback)
| Property | Value |
|---|
| Source | Forschungsstelle für Energiewirtschaft (FfE), Munich |
| Data | Modeled projection — 8760 hourly values from the Dynamis energy scenario model, based on weather reference year 2012 |
| Storage | Azure blob storage (no rate limits): ffeopendatastorage.blob.core.windows.net |
| License | CC-BY-4.0 |
| Year selection | Nearest available projection year (2020, 2025, 2030, 2035, 2040, 2045, 2050) |
| Cache | In-memory, 24-hour TTL (data is static per year) |
| Methodology string | ffe-projection |
These are modeled projections, NOT actual measurements. The simulation uses weather reference year 2012 to produce a plausible hourly carbon intensity profile. This means:
- The same hour-of-year always returns the same value, regardless of when you query
- It captures realistic seasonal and diurnal patterns (e.g., midday solar dips, winter peaks)
- It cannot reflect today’s actual wind speed, cloud cover, or demand conditions
Note: FfE projection data is only available for Germany (zone DE). For other zones, Tier 2 is skipped and the chain falls through directly to Tier 3.
The data is produced under the InDEED research project (Integrating Decentralized Energy Data).
Reference: FfE OpenData (InDEED project)
Tier 3: Static Lookup Table (last resort)
A 192-value lookup table for all 13 supported zones, indexed by season (4), day type (weekday/weekend), and hour (24). Derived from Energy Charts /public_power data (2025–2026, IPCC AR5 emission factors).
| Pattern | Range (gCO2/kWh) | Cause |
|---|
| Summer midday (10:00–14:00) | 220–265 | High solar generation (DE example) |
| Summer night (00:00–05:00) | 470–525 | Fossil baseload (DE example) |
| Winter (all day) | 350–435 | Flatter, wind-dependent (DE example) |
| Weekend vs. weekday | 10–20% lower | Reduced industrial demand |
Methodology string: static
Methodological basis:
- Kono, J., Ostermeyer, Y. & Wallbaum, H. (2017). “The trends of hourly carbon emission factors in Germany and investigation on relevant consumption patterns for its application.” International Journal of Life Cycle Assessment, 22, 1493–1501. DOI: 10.1007/s11367-017-1277-z
- Holzapfel, P., Bach, V. & Finkbeiner, M. (2023). “Increasing temporal resolution in greenhouse gas accounting of electricity consumption divided into Scopes 2 and 3.” International Journal of Life Cycle Assessment, 28, 1622–1639. DOI: 10.1007/s11367-023-02240-3
Last-Resort Fallback
| Parameter | Value |
|---|
| Intensity | 380 gCO2eq/kWh |
Updated from 400 g/kWh in v0.2.x to reflect declining German grid intensity. Per Umweltbundesamt (UBA), the annual average was 386 g/kWh (2023) and 363 g/kWh (2024). The carbon_source field is set to "fallback" to flag this condition.
Reference: Umweltbundesamt — Strom- und Wärmeversorgung in Zahlen
Provider Selection
The --carbon-provider flag (or RUNNER_CARBON_PROVIDER env var) controls which providers are used:
| Value | Behavior | Use Case |
|---|
energy-charts (default) | Full 3-tier chain: Energy Charts → FfE projection → static table | Best accuracy; requires internet access |
ffe | Legacy alias — creates the same full 3-tier chain as energy-charts | Backward compatibility |
static | Static lookup table only (no external dependencies) | Air-gapped environments, deterministic testing |
Multi-Country Carbon Zones
CI Sizer supports 13 European electricity grid zones for carbon intensity estimation. The zone is configured via the --carbon-zone flag or RUNNER_CARBON_ZONE environment variable (default: DE).
Supported Zones
| Zone | Country | Typical CI (gCO₂eq/kWh) | Notes |
|---|
AT | Austria | ~67 | Hydro-dominated |
BE | Belgium | ~179 | Gas + nuclear mix |
CH | Switzerland | ~42 | Hydro + nuclear |
DE | Germany | ~258 | Mixed (coal/gas/wind/solar) |
DK | Denmark | ~88 | Wind-heavy |
ES | Spain | ~94 | Solar + wind + gas |
FI | Finland | ~59 | Nuclear + hydro + biomass |
FR | France | ~24 | Nuclear-dominated |
IT | Italy | ~206 | Gas-heavy |
NL | Netherlands | ~369 | Gas-dominated |
NO | Norway | ~28 | Hydro-dominated |
PL | Poland | ~505 | Coal-dominated |
SE | Sweden | ~33 | Hydro + nuclear |
Configuration
# French grid (nuclear-dominated, very low CI)
./collector --carbon-zone FR
# Or via environment variable
RUNNER_CARBON_ZONE=PL ./collector
Provider Chain by Zone
The fallback chain differs depending on the selected zone:
- DE (Germany): Energy Charts → FfE blob projection → static (seasonal/weekday table with 192 values)
- All other zones: Energy Charts → static (seasonal/weekday table with 192 values)
FfE projection data (Tier 2) is only available for Germany. For all other zones, the provider chain skips FfE and falls back directly to the static table. All 13 supported zones have full 192-value static tables (4 seasons × 2 day types × 24 hours) derived from Energy Charts data. If all providers fail, the hardcoded 380 gCO₂/kWh fallback is used regardless of zone.
CPU TDP Database
A built-in database of 38 CPU models maps processor names to TDP (Thermal Design Power) values:
| Family | Generations | TDP Range |
|---|
| Intel Xeon Platinum | Skylake, Cascade Lake, Ice Lake, Sapphire Rapids | 195–350 W |
| Intel Xeon Gold | Various | 165–205 W |
| Intel Xeon Silver | Various | 100–135 W |
| Intel Xeon E5 | Ivy Bridge, Haswell, Broadwell | 115–145 W |
| AMD EPYC Rome | 7000-series | 225–280 W |
| AMD EPYC Milan | 7000-series | 225–280 W |
| AMD EPYC Genoa | 9000-series | 360 W |
| AWS Graviton | Graviton2, Graviton3, Graviton4 | 130–210 W |
| Ampere | Altra, AmpereOne | 160–210 W |
Sources:
Graviton TDP values (Graviton2 ~130 W, Graviton3 ~180 W, Graviton4 ~210 W) are engineering estimates based on ARM Neoverse power characteristics, not manufacturer specifications.
Confidence Levels
Each energy score includes a confidence field indicating the quality of the estimate:
| Level | Hardware Source | Typical Accuracy |
|---|
user-provided | User explicitly specified hardware | Depends on user |
auto-detected | CPU model matched in TDP database | ±15–20% |
generic-estimate | Fell back to average cloud defaults | ±50% |
The confidence level reflects the hardware profile quality. Carbon data source quality is captured separately in the carbon_source field (energy-charts, ffe-projection, static, or fallback).
Limitations
| Limitation | Impact |
|---|
| Statistical approximation | Power estimates are modeled, not measured from hardware power meters |
| CPU-only power model | Memory, storage, network, and GPU power are not modeled separately |
| Carbon intensity variability | Hourly/15-min data is preferred over annual averages; actual intensity varies by time of day and season |
| Energy Charts emission factors | IPCC AR5 lifecycle emission factors per fuel type are median values; actual plant-level emissions vary, giving ±10–15% uncertainty |
| FfE projection data | Based on the Dynamis energy scenario model; projection years (2025, 2030, etc.) may not match actual grid conditions |
| Graviton/ARM TDP estimates | Not manufacturer specifications; based on ARM Neoverse power characteristics |
| Uniform PUE | Single global default; actual PUE varies by datacenter location, load, and ambient temperature |
| Limited zone support | Carbon intensity available for 13 European zones via Energy Charts (DE, AT, FR, NL, PL, DK, CH, ES, IT, BE, SE, NO, FI). FfE projection data is DE-only. Static fallback covers all 13 zones with full seasonal/weekday/hourly resolution. Unsupported zones use 380 gCO₂/kWh fallback |
| Average utilization | Mean CPU utilization over the run smooths out short spikes |
Verifying the Data
You can query the upstream carbon intensity sources directly to verify what CI Sizer is seeing.
Reading the Methodology String
Each energy score in CI Sizer includes a methodology string like:
ccf-linear+energy-charts+DE+pue-1.30
| Part | Meaning |
|---|
ccf-linear | Power model — Cloud Carbon Footprint linear interpolation (vCPUs × watts) |
energy-charts | Carbon intensity source that successfully returned data |
DE | Electricity grid zone |
pue-1.30 | Power Usage Effectiveness multiplier (1.3× datacenter overhead) |
If the carbon source shows ffe-projection or static instead of energy-charts, the system fell back because Energy Charts was unavailable.
Querying Energy Charts (Tier 1)
The Energy Charts API requires lowercase country codes and date-only format (no timestamps):
# German grid generation mix for today (replace dates with today/tomorrow)
curl -s "https://api.energy-charts.info/public_power?country=de&start=2026-05-20&end=2026-05-21" \
| jq '{
timestamps: (.unix_seconds | length),
first: (.unix_seconds[0] | todate),
last: (.unix_seconds[-1] | todate),
fuels: [.production_types[] | .name]
}'
# See actual MW per fuel type at a specific timestamp
curl -s "https://api.energy-charts.info/public_power?country=de&start=2026-05-20&end=2026-05-21" \
| jq '{
time: (.unix_seconds[40] | todate),
generation_MW: [.production_types[] | select(.data[40] != null and .data[40] > 0) | {(.name): (.data[40] | round)}] | add
}'
# French grid (nuclear-dominated, very low carbon)
curl -s "https://api.energy-charts.info/public_power?country=fr&start=2026-05-20&end=2026-05-21" \
| jq '[.production_types[] | .name]'
The response contains unix_seconds[] (15-minute intervals) and production_types[{name, data[]}] with MW output per fuel type. CI Sizer multiplies each fuel’s MW by its IPCC AR5 emission factor and divides by total generation to compute gCO₂eq/kWh.
Important: Replace the dates in the examples with today’s date. The API returns data up to the most recent 15-minute interval.
Computing Grid Carbon Intensity
To replicate the exact calculation CI Sizer performs — weighted average of generation MW × emission factor:
# Compute current German grid carbon intensity (same formula as ci-sizer)
curl -s "https://api.energy-charts.info/public_power?country=de&start=$(date -u +%Y-%m-%d)&end=$(date -u -v+1d +%Y-%m-%d)" | jq '
(.unix_seconds | length - 1) as $idx |
(.unix_seconds[$idx] | todate) as $time |
{"Fossil brown coal / lignite":1054,"Fossil hard coal":888,"Fossil gas":410,
"Fossil oil":733,"Fossil coal-derived gas":850,"Fossil peat":1100,
"Nuclear":12,"Biomass":230,"Geothermal":38,"Wind offshore":12,
"Wind onshore":11,"Solar":45,"Hydro Run-of-River":24,
"Hydro water reservoir":24,"Hydro pumped storage":24,
"Waste":330,"Others":400,"Other renewables":30} as $f |
[.production_types[] | select(.data[$idx]!=null and .data[$idx]>0) |
{name,mw:.data[$idx]} | select($f[.name]!=null) |
{name,mw,w:(.mw*$f[.name])}] as $g |
{timestamp: $time,
grid_carbon_intensity_gCO2_per_kWh: ([$g[]|.w]|add)/([$g[]|.mw]|add)|round,
total_generation_MW: ([$g[]|.mw]|add)|round,
top_contributors: [$g | sort_by(-.w)[0:5][] | {fuel:.name, MW:(.mw|round), share_pct:((.w/([$g[]|.w]|add)*100)*10|round/10)}]}'
This takes the most recent 15-minute interval, multiplies each fuel type’s MW output by its IPCC emission factor, sums the weighted values, and divides by total generation to get gCO₂eq/kWh. The top_contributors field shows which fuels are driving most of the carbon impact.
Note: On Linux, replace date -v+1d with date -d "+1 day". Or simply hardcode tomorrow’s date.
Querying FfE Projection (Tier 2)
The FfE blob contains 8760 hourly projection values for an entire year (DE only):
# Get 2025 projection metadata
curl -s "https://ffeopendatastorage.blob.core.windows.net/opendata/id_opendata_2/id_opendata_2_year_2025.json" \
| jq '[.[] | select(.internal_id == [2,1,1])][0] | {
year: .year,
weather_reference_year: .year_weather,
total_hours: (.values | length),
annual_average_gCO2_per_kWh: (.value * 1000 | round),
unit: "values are in kg/kWh, multiply by 1000 for g/kWh"
}'
# Get projection for a specific hour (e.g., May 20 at 09:00 UTC = hour index 3345)
curl -s "https://ffeopendatastorage.blob.core.windows.net/opendata/id_opendata_2/id_opendata_2_year_2025.json" \
| jq '[.[] | select(.internal_id == [2,1,1])][0] | {
hour_index: 3345,
carbon_intensity_gCO2_per_kWh: (.values[3345] * 1000 | round),
note: "This is a modeled projection, not real-time data"
}'
Note: Hour index = (day_of_year - 1) × 24 + hour_utc. These values are static projections based on weather year 2012 — they will return the same result regardless of when you query them.
Tier 3 (Static Table)
The static fallback table is compiled into the binary — there is no external API to query. It provides 192 values per zone (4 seasons × 2 day types × 24 hours) derived from Energy Charts historical data.
6 - API Reference
REST API reference for the CI Sizer collector and receiver.
Overview
All endpoints are under /api/v1 unless noted otherwise. The OpenAPI specification is auto-generated and served live at /swagger on the receiver. The spec file is also available at docs/openapi.json in the repository.
Authentication
CI Sizer uses a two-tier token system:
- Read token (
--read-token): Pre-shared admin credential for read/query endpoints. Used as Authorization: Bearer <read-token>. - Push tokens (derived from
--hmac-key): Scoped, time-limited HMAC-SHA256 tokens for collectors. Generated via POST /api/v1/token.
Authentication behaviour depends on the configured auth mode. See Configuration — Authentication Modes for details.
Authentication by Endpoint (OIDC Mode)
In oidc mode, endpoints use two tiers. The relaxed tier also accepts a Bearer read token, enabling programmatic access (e.g., from the GARM provider).
| Endpoint | Auth Tier | OIDC Session | Bearer Read Token |
|---|
GET /health | None | — | — |
POST /api/v1/token | Read token | — | Yes |
POST /api/v1/metrics | Push token | — | — |
GET /api/v1/sizing/* | Relaxed | Yes | Yes |
GET /api/v1/runners/overview | Relaxed | Yes | Yes |
GET /api/v1/runners/{runner} | Relaxed | Yes | Yes |
| All other protected endpoints | Strict | Yes | No |
In gateway mode, replace “OIDC Session” with “Gateway JWT (X-Access-Token)”. In none mode, all protected endpoints accept the Bearer read token.
Health and Info
No authentication required.
| Method | Path | Description |
|---|
GET | /health | Health check |
GET | /api/v1/info | Service info (version, auth mode, CI provider, forgejo_base_url, logout_url) |
Token and Metrics Ingest
| Method | Path | Auth | Description |
|---|
POST | /api/v1/token | Read token | Generate a scoped push token |
POST | /api/v1/metrics | Push token | Receive and store a metric summary |
Push Token Generation
curl -s -X POST http://localhost:8080/api/v1/token \
-H "Authorization: Bearer <read-token>" \
-H "Content-Type: application/json" \
-d '{"organization":"my-org","repository":"my-repo","workflow":"ci.yml","job":"build"}'
The returned token is scoped to the specified org/repo/workflow/job combination and expires after the configured --token-ttl (default: 2 hours).
Metrics Query
| Method | Path | Description |
|---|
GET | /api/v1/metrics/repo/{org}/{repo}/{workflow}/{job} | Query stored metrics for a workflow/job |
GET | /api/v1/metrics/runner/{runner} | Query stored metrics for a specific runner |
GET | /api/v1/debug/metrics | Dump all metric rows from the database |
Metrics Response Example
[
{
"id": 1,
"organization": "my-org",
"repository": "my-org/my-repo",
"workflow": "ci.yml",
"job": "build",
"run_id": "run-123",
"received_at": "2026-02-06T14:30:23.056Z",
"payload": {
"start_time": "2026-02-06T14:30:02.185Z",
"end_time": "2026-02-06T14:30:22.190Z",
"duration_seconds": 20.0,
"sample_count": 11,
"containers": [
{
"name": "runner",
"cpu_cores": { "peak": 2.007, "avg": 1.5, "p50": 1.817, "p95": 2.004 },
"memory_bytes": { "peak": 18567168, "avg": 18567168 }
}
]
}
}
]
CPU metric distinction:
cpu_total_percent — system-wide, 0–100%cpu_cores (containers) — cores used (e.g., 2.0 = two full cores)peak_cpu_percent (processes) — per-process, where 100% = 1 core
All memory values are in bytes.
Sizing
| Method | Path | Description |
|---|
GET | /api/v1/sizing/repo/{org}/{repo}/{workflow}/{job} | Compute container sizes from historical data |
Query Parameters
| Parameter | Default | Description |
|---|
runs | 5 | Number of recent runs to analyse (1–100) |
buffer | 20 | CPU headroom percentage (memory uses a staircase buffer) |
cpu_percentile | p95 | CPU stat to use: peak, p99, p95, p75, p50, avg |
Sizing Response Example
{
"containers": [
{
"name": "runner",
"cpu": { "request": "960m", "limit": "1", "enforced": false },
"memory": { "request": "1024Mi", "limit": "1024Mi", "enforced": true }
}
],
"total": {
"cpu": { "request": "970m", "limit": "1500m" },
"memory": { "request": "647Mi", "limit": "1024Mi" }
},
"meta": {
"runs_analyzed": 10,
"buffer_percent": 20,
"cpu_percentile": "p95",
"cpu_sizing_mode": "observe",
"memory_qos": "guaranteed"
}
}
For details on the sizing algorithm, buffers, and enforcement modes, see Sizing Algorithm.
Energy and Carbon
| Method | Path | Description |
|---|
GET | /api/v1/energy/repo/{org}/{repo}/{workflow}/{job} | Energy/carbon estimates for a workflow/job |
Supports ?from= and ?to= time range filters (ISO 8601).
Aggregation and Dashboard
All aggregation endpoints support ?from=, ?to= (ISO 8601) and ?limit= / ?offset= pagination.
| Method | Path | Description |
|---|
GET | /api/v1/metrics/overview | Global KPI summary with per-org breakdown |
GET | /api/v1/metrics/org/{org} | Org detail with per-repo summaries |
GET | /api/v1/metrics/org/{org}/repo/{repo} | Repo detail with per-workflow summaries |
GET | /api/v1/sizing/org/{org} | Org-wide sizing overview |
GET | /api/v1/sizing/org/{org}/repo/{repo} | Repo-wide sizing overview |
GET | /api/v1/compare/repos | Cross-repo KPI comparison (?org=) |
GET | /api/v1/compare/workflows | Cross-workflow KPI comparison (?org=&repo=) |
GET | /api/v1/runners | List known runners |
GET | /api/v1/runners/overview | Runner fleet overview |
GET | /api/v1/runners/{runner} | Runner detail with per-org breakdown |
GET | /api/v1/success-failure-stats/repo/{org}/{repo}/{workflow}/{job} | Pass/fail statistics |
Sizing Overrides
| Method | Path | Description |
|---|
GET | /api/v1/sizing/overrides | List all overrides |
PUT | /api/v1/sizing/overrides/{org} | Upsert org-level override |
PUT | /api/v1/sizing/overrides/{org}/{repo} | Upsert repo-level override |
PUT | /api/v1/sizing/overrides/{org}/{repo}/{workflow} | Upsert workflow-level override |
PUT | /api/v1/sizing/overrides/{org}/{repo}/{workflow}/{job} | Upsert job-level override |
DELETE | Same paths as PUT | Remove override at that scope |
Override hierarchy: job > workflow > repo > org (most-specific wins). Fields left null are inherited from the next parent scope. See Sizing Algorithm — Sizing Overrides for details.
OIDC UI Routes
Available when auth mode is oidc.
| Method | Path | Description |
|---|
GET | /ui/login | Initiate OIDC login |
GET | /ui/callback | OIDC callback |
GET | /ui/logout | Logout |
GET | /ui/me | Current user info |
OpenAPI Specification
The OpenAPI spec is auto-generated from the receiver’s route definitions and served live at /swagger. The spec file is also committed to the repository at docs/openapi.json.
Note: The OpenAPI spec is generated code — do not edit it manually. Run make openapi in the ci-sizer repository to regenerate it after API changes.
7 - GitLab CI Integration
Integrating CI Sizer with GitLab CI via the MutatingAdmissionWebhook.
Overview
CI Sizer supports GitLab CI through the gitlab-webhook-edge-connect component — a Kubernetes MutatingAdmissionWebhook that intercepts GitLab Runner executor pods and injects the CI Sizer collector sidecar.
Unlike Forgejo/GitHub Actions (which use GARM for runner lifecycle management), GitLab Runner uses its own Kubernetes executor. The webhook intercepts pods at admission time and mutates them to include the collector.
Repository: edp.buildth.ing/DevFW-CICD/gitlab-webhook-edge-connect
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Kubernetes API Server │
│ │
│ MutatingAdmissionWebhook │
│ ┌────────────────────────────────────┐ │
│ │ gitlab-webhook-edge-connect │ │
│ │ │ │
│ │ Intercepts pods with label: │ │
│ │ job.runner.gitlab.com/pod (Exists)│ │
│ │ │ │
│ │ Injects: collector sidecar │ │
│ │ Sets: shareProcessNamespace=true │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Pod Targeting
The webhook targets GitLab Runner pods using a label selector (not annotation):
objectSelector:
matchExpressions:
- key: job.runner.gitlab.com/pod
operator: Exists
This label is automatically applied by the GitLab Runner Kubernetes executor to all job pods.
Design Decision
Label-based targeting was chosen over annotation-based targeting because MutatingAdmissionWebhook objectSelector only supports label selectors. This provides efficient server-side filtering without requiring the webhook to inspect every pod creation.Backends
The webhook supports two mutation backends:
| Backend | Description |
|---|
| KubernetesBackend | Inline mutation — directly patches the pod spec to add the collector sidecar |
| EdgeConnectBackend | SDK-based provisioning — provisions resources via the EdgeConnect SDK |
Collector Injection
The collector is injected using the shared library ci-sizer/pkg/inject, which is common across all CI providers. The injection adds:
- A collector sidecar container
shareProcessNamespace: true on the pod spec- Appropriate environment variables for CI context
Cgroup Exclusion Strategy
GitLab Runner pods present a unique challenge: the build container’s process name varies by image (it could be sh, bash, pwsh, or any custom entrypoint). This makes positive identification by process name impossible.
CI Sizer solves this with an exclusion strategy:
- Map all known containers by process name (e.g.,
gitlab-runner-helper, collector) - Any remaining cgroup paths that don’t match a known container are assigned to the build container
This is configured via:
CGROUP_STRATEGY=exclusion
Note
The GitLab Runner helper process name (gitlab-runner-helper) is truncated to 15 characters in /proc/PID/status due to the Linux kernel’s Name field limit. The exclusion strategy accounts for this truncation.Run Index
For GitLab (non-GARM) providers, the run_index is assigned by the receiver using a MaxRunIndex+1 counter per org/repo/workflow combination. This provides sequential run numbering without requiring GARM lifecycle events.
Run URL
The run URL is propagated via the pod annotation job.runner.gitlab.com/url, which the collector reads at startup.
Runner Name
For GitLab, the runner_name is set to the pod name (pod.Name), since GitLab Runner pods are ephemeral and uniquely named per job.
Deployment
The webhook is deployed to the ci-sizer namespace with TLS provided by cert-manager using a self-signed issuer:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: gitlab-webhook-tls
namespace: ci-sizer
spec:
secretName: gitlab-webhook-tls
issuerRef:
name: ci-sizer-selfsigned
kind: Issuer
dnsNames:
- gitlab-webhook-edge-connect.ci-sizer.svc
- gitlab-webhook-edge-connect.ci-sizer.svc.cluster.local
GitLab-Specific Configuration
| Variable | Description |
|---|
CI_SIZER_RUNNER_NAME | Override runner name (defaults to pod name) |
CGROUP_STRATEGY | Set to exclusion for GitLab pods |
CI_PROVIDER | Set to gitlab |
For commit status notifications to GitLab, see OOM Detection — Commit Status Notifications.
8 - KPI Benchmark
Benchmark methodology and results demonstrating CI Sizer’s resource optimization and energy savings.
Overview
The KPI Benchmark validates CI Sizer’s effectiveness through a controlled experiment measuring resource utilization, energy consumption, scheduling density, and reliability across multiple workload types and sizing conditions.
Repository: edp.buildth.ing/DevFW/kpi-benchmark
Methodology
Experimental Design
The benchmark uses a factorial design:
- 5 conditions × 3 workloads × 30 runs = 450 total runs
Conditions
| Condition | Description |
|---|
STATIC | Fixed resource allocations (baseline — no sizer) |
GARM_BARE | GARM runner provisioning without sizer |
GARM_OBSERVE | Sizer in observe mode (recommendations computed, not enforced) |
GARM_ENFORCE | Sizer in enforce mode (recommendations applied as K8s requests/limits) |
GARM_WARM | Sizer enforce mode with pre-warmed historical data |
Workloads
| Workload | Description |
|---|
carbon-burner | CPU stress workload |
memory-stress | Variable memory allocation workload |
go-build | Real-world multi-package Go compilation |
Statistical Approach
- Bootstrap BCa confidence intervals for resource metrics
- Fisher’s exact test for OOM rate comparisons
- Paired Wilcoxon signed-rank tests for duration comparisons
Key Results
Resource Optimization
| Metric | Improvement |
|---|
| CPU oversizing reduction | 79.64% |
| Memory oversizing reduction | 88.59% |
| Scheduling density improvement | 12.2× (500m → 41m CPU requests) |
Baseline Waste
Without CI Sizer, typical CI workloads exhibit significant resource waste:
| Workload | CPU Utilization | Waste |
|---|
| Batch (carbon-burner) | 12.8% | 87% |
| Go build | 48.4% | 52% |
Energy
- Per-run energy: 0.095–0.323 mWh (measured via CCF methodology)
- Projected savings at scale: 65–90% energy reduction via node-hour reduction
Reliability
| Scenario | Completion Rate |
|---|
| Without sizer (variable-memory workloads) | 60% |
| With sizer | 100% |
OOM without the sizer causes node eviction and collateral damage to co-located pods. With the sizer, failures are contained at the cgroup boundary.
| Mode | Overhead |
|---|
| Observe mode | <1% duration overhead, zero resource modification |
| Enforce mode (go-build) | 1–5% faster (tighter limits reduce scheduling contention) |
| GARM lifecycle (lightweight workloads) | 7–20% duration overhead |
The benchmark validates the following IPCEI-CIS work package objectives:
| KPI | Work Package | Objective | Target | Result |
|---|
| Resource utilization | WP e.1 | OB 45/46 | ≥10% improvement | 79–89% improvement |
| Sustainability | WP e.2 | OB 47/48 | ≥10% improvement | 65–90% projected |