This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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

ProviderInjection Mechanismci_provider value
Forgejo ActionsGARM runner lifecycleforgejo
GitHub ActionsGARM runner lifecyclegithub
GitLab CIMutatingAdmissionWebhookgitlab

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

  1. Review the Configuration reference for all collector and receiver flags
  2. Deploy the receiver as a central service and the collector as a sidecar in your CI pods
  3. Generate scoped push tokens for each workflow/job combination
  4. Access the Web Dashboard at /ui to explore metrics and sizing recommendations

For Kubernetes deployment examples, see the Configuration page.

Repository

Documentation

GuideDescription
ConfigurationAll collector and receiver flags, environment variables, deployment examples
Web DashboardUsing the web UI for resource analysis and sizing recommendations
Sizing AlgorithmAlgorithm steps, buffers, floors, overrides, enforcement modes
OOM DetectionConfidence-gated sizing, OOM recovery, commit status notifications
Energy EstimationPower models, carbon sources, TDP database, and academic references
GitLab IntegrationMutatingAdmissionWebhook setup for GitLab CI
KPI BenchmarkBenchmark methodology and resource optimization results
API ReferenceAll 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

FlagEnvironment VariableDescriptionDefault
--intervalCollection interval (e.g., 5s, 1m)5s
--proc-pathPath to proc filesystem/proc
--log-levelLog level: debug, info, warn, errorinfo
--log-formatOutput format: json, textjson
--topNumber of top processes to include5
--push-endpointHTTP endpoint to push metrics to
--push-tokenCOLLECTOR_PUSH_TOKENBearer token for push endpoint auth
--hardware-profileRUNNER_HARDWARE_PROFILEHardware profile: JSON, preset name, or empty for auto-detectauto-detect
--carbon-providerRUNNER_CARBON_PROVIDERCarbon intensity provider: energy-charts (default, full 3-tier: Energy Charts → FfE → static), ffe (legacy alias, same chain), static (static table only)energy-charts
--carbon-zoneRUNNER_CARBON_ZONECarbon intensity zoneDE
--pueRUNNER_PUEPower Usage Effectiveness multiplier1.3

Carbon Zone

The carbon zone determines which electricity grid is used for carbon intensity estimation.

FlagEnvDefaultDescription
--carbon-zoneRUNNER_CARBON_ZONEDEISO 3166-1 alpha-2 country code for the electricity grid zone

Supported zones:

ZoneCountryTypical CI (gCO₂eq/kWh)Notes
ATAustria~67Hydro-dominated
BEBelgium~179Gas + nuclear mix
CHSwitzerland~42Hydro + nuclear
DEGermany~258Mixed (coal/gas/wind/solar)
DKDenmark~88Wind-heavy
ESSpain~94Solar + wind + gas
FIFinland~59Nuclear + hydro + biomass
FRFrance~24Nuclear-dominated
ITItaly~206Gas-heavy
NLNetherlands~369Gas-dominated
NONorway~28Hydro-dominated
PLPoland~505Coal-dominated
SESweden~33Hydro + 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.

VariableDescriptionExample
GITHUB_REPOSITORY_OWNEROrganization namemy-org
GITHUB_REPOSITORYFull repository pathmy-org/my-repo
GITHUB_WORKFLOWWorkflow filenameci.yml
GITHUB_JOBJob namebuild
GITHUB_RUN_IDUnique run identifierrun-123
CGROUP_PROCESS_MAPJSON: process name to container name{"node":"runner"}
CGROUP_LIMITSJSON: per-container CPU/memory limitsSee below

CGROUP_LIMITS Format

{
  "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

FlagEnvironment VariableDescriptionDefault
--addrHTTP listen address:8080
--dbSQLite database pathmetrics.db
--read-tokenRECEIVER_READ_TOKENPre-shared token for read/admin endpoints
--hmac-keyRECEIVER_HMAC_KEYSecret key for push token generation/validation
--token-ttlTime-to-live for push tokens2h
--auth-modeRECEIVER_AUTH_MODEAuthentication mode: none, oidc, gatewayauto-detect
--cpu-sizing-modeRECEIVER_CPU_SIZING_MODECPU sizing mode: observe or enforceobserve
--memory-qosRECEIVER_MEMORY_QOSMemory QoS class: guaranteed or burstableguaranteed
--log-levelRECEIVER_LOG_LEVELLog level: debug, info, warn, errorinfo

OIDC / Authentication Flags

FlagEnvironment VariableDescriptionDefault
--oidc-issuerRECEIVER_OIDC_ISSUEROIDC issuer URL
--oidc-client-idRECEIVER_OIDC_CLIENT_IDOIDC client ID
--oidc-client-secretRECEIVER_OIDC_CLIENT_SECRETOIDC client secret
--oidc-redirect-uriRECEIVER_OIDC_REDIRECT_URIOIDC redirect URI
--session-ttlSession cookie TTL12h
--session-signing-keyRECEIVER_SESSION_SIGNING_KEYHex-encoded 32-byte session signing keyauto-generate
--cookie-secureRECEIVER_COOKIE_SECURESet Secure flag on auth cookies; disable for plain HTTPtrue
--allowed-orgRECEIVER_ALLOWED_ORGAllowed organization for OIDC login
--logout-urlRECEIVER_LOGOUT_URLExternal logout URL for gateway mode

JWT Claim Mapping

FlagEnvironment VariableDescriptionDefault
--claim-subRECEIVER_CLAIM_SUBJWT claim for user IDsub
--claim-nameRECEIVER_CLAIM_NAMEJWT claim for display namename
--claim-emailRECEIVER_CLAIM_EMAILJWT claim for emailemail
--claim-groupsRECEIVER_CLAIM_GROUPSJWT claim for groups arraygroups
--claim-orgRECEIVER_CLAIM_ORGJWT claim for organizationorg
--org-from-groupsRECEIVER_ORG_FROM_GROUPSOrg extraction from groups: first, match, nonefirst

GARM Integration Flags

FlagEnvironment VariableDescriptionDefault
--garm-urlGARM_URLGARM base URL for WebSocket event enrichment
--garm-userGARM_USERGARM username for JWT authentication
--garm-passwordGARM_PASSWORDGARM password for JWT authentication
--garm-cache-ttlGARM_CACHE_TTLTTL for pending GARM event cache60s

OOM Detection & Notification Flags

FlagEnvironment VariableDescriptionDefault
--max-memoryRECEIVER_MAX_MEMORYNode ceiling for memory (overrides auto-detection from /proc/meminfo)auto (90% node RAM)
--max-cpuRECEIVER_MAX_CPUNode ceiling for CPUauto
--notify-enabledRECEIVER_NOTIFY_ENABLEDEnable commit status notifications on OOMfalse
--notify-base-urlRECEIVER_NOTIFY_BASE_URLForge base URL (auto-detected if unset)
--notify-tokenRECEIVER_NOTIFY_TOKENAPI token for forge commit status API

Multi-Provider Flags

FlagEnvironment VariableDescriptionDefault
--ci-providerCI_PROVIDERCI provider: forgejo, github, gitlabforgejo

GitLab-Specific Variables

These variables are relevant when CI_PROVIDER=gitlab:

VariableDescriptionDefault
CI_SIZER_RUNNER_NAMEOverride runner name (defaults to pod name for GitLab)pod name
CGROUP_STRATEGYCgroup mapping strategy: default or exclusiondefault

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:

ModeDescription
noneToken-only authentication. All protected endpoints accept the Bearer read token.
oidcDirect 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.
gatewayExternal 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.

The dashboard uses a hierarchical drill-down model with card-based navigation:

  1. Overview — global KPI summary with per-organization breakdown
  2. Organization — per-repository summaries within an org
  3. Repository — per-workflow summaries within a repo
  4. Workflow/Job — charts showing resource usage, sizing, and energy data across runs
  5. 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

KeyAction
?Show keyboard shortcuts help
/Focus entity search
BackspaceNavigate up one level
EscClose modal
CToggle compare basket
OOpen compare view (when basket has items)
Cmd+D / Ctrl+DToggle dark mode
Enter / SpaceActivate focused card
Left / Right arrowsNavigate 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):

  1. Press C to open the compare basket
  2. Add entities to the basket from any navigation level
  3. Press O or click the compare button to view a side-by-side comparison
  4. Compare view shows KPIs, resource usage trends, and sizing recommendations across selected entities

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:

PhaseClean SamplesBehaviour
unknown0Returns bootstrap default: 4Gi memory, 500m CPU
learning1–2Applies 3× headroom above observed peak (conservative)
confident≥3Full algorithm with tight staircase buffer

In the confident phase, the full algorithm below applies:

  1. Collect the N most recent runs (configurable via ?runs= query parameter, 1–100)
  2. 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
  3. Apply buffers to add headroom above observed values
  4. Apply floor values to ensure minimum viable allocations
  5. Apply a memory ceiling — no single container can exceed the total pod memory observed across all runs (plus buffer)
  6. 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

ParameterDefaultDescription
runs5Number of recent runs to analyse (1–100)
buffer20CPU headroom percentage (memory uses the staircase below)
cpu_percentilep95CPU 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:

ResourceRequest FloorLimit Floor
CPU10m500m
Memory32Mi128Mi

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 MemoryBuffer
< 1 GiB20%
1 – 4 GiB10%
> 4 GiB5%

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:

ModeDescription
observe (default)Compute CPU recommendations and report them, but mark enforced: false. The provider uses its own defaults.
enforceApply CPU recommendations as Kubernetes requests/limits (enforced: true).

Memory QoS

The --memory-qos flag controls the memory QoS class:

ModeDescription
guaranteed (default)Memory request equals memory limit (Guaranteed QoS class). Prevents overcommit.
burstableMemory 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

MethodPathDescription
GET/api/v1/sizing/overridesList 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
DELETESame paths as PUTRemove 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:

PhaseConditionBehaviour
unknown0 clean samplesReturns a bootstrap default of 4Gi memory. API responds with HTTP 200 and meta.confidence_phase == "unknown".
learning1–2 clean samplesApplies 3× headroom above observed peak. Conservative to avoid OOMs while data is sparse.
confident≥3 clean samplesUses the tight staircase buffer (20%/10%/5%). Full algorithm precision.

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:

  1. Auto-detection — reads /proc/meminfo and uses 90% of total node RAM
  2. 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

FlagEnvironment VariableDescriptionDefault
--notify-enabledRECEIVER_NOTIFY_ENABLEDEnable commit status notificationstrue
--notify-base-urlRECEIVER_NOTIFY_BASE_URLForge base URL (auto-detected from push metadata if unset)
--notify-tokenRECEIVER_NOTIFY_TOKENAPI 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

FilePurpose
internal/cgroup/oom.goCgroup v2 OOM detection via memory.events
internal/receiver/sizing/confidence.goConfidence phase logic and phase transitions
internal/receiver/notify/notify.goCommit 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 UtilizationPower 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):

ParameterValueMeaning
MinWatts0.74 W/vCPUIdle power per vCPU
MaxWatts3.50 W/vCPUMax-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

ConditionModel 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)

ParameterValue
Default1.3

A PUE of 1.3 means the datacenter uses 30% more energy than the IT equipment alone. This is a conservative middle ground:

ContextTypical PUE
Hyperscalers (Google, AWS, Azure)1.10–1.18
New datacenter builds (Uptime 2024)~1.3
Industry average1.56

Provider-specific PUEs (from Cloud Carbon Footprint):

ProviderPUE
AWS1.135
GCP1.1
Azure1.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

Energy Charts (Tier 1)FfE Projection (Tier 2)Static Table (Tier 3)
Data typeReal MW from actual power plantsModeled scenario projectionDerived seasonal averages
UpdatesEvery 15 minutesStatic per projection yearNever (compiled into binary)
AccuracyActual grid state right nowWeather-year-2012 estimateSeasonal/hourly average
Zones13 EU countriesDE only13 EU countries
AvailabilitySometimes delayed or unavailableAlways availableAlways 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)

PropertyValue
SourceFraunhofer Institute for Solar Energy Systems (Fraunhofer ISE)
APIhttps://api.energy-charts.info/public_power?country={cc}
DataReal-time actual generation — MW output from real power plants operating right now
Resolution15-minute intervals, updated continuously
AuthenticationNone required
CacheIn-memory, 15-minute TTL (matching data resolution)
Methodology stringenergy-charts
Supported zonesDE, 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/kWhSource
Fossil peat1100IPCC AR5
Fossil brown coal / lignite1054IPCC AR5
Fossil hard coal888IPCC AR5
Fossil coal-derived gas850IPCC AR5
Fossil oil733IPCC AR5
Fossil gas410IPCC AR5
Others400Conservative estimate
Waste330IPCC AR5 (mixed waste)
Biomass230IPCC AR5
Solar45IPCC AR5
Geothermal38IPCC AR5
Other renewables30IPCC AR5
Hydro Run-of-River24IPCC AR5
Hydro water reservoir24IPCC AR5
Hydro pumped storage24IPCC AR5
Nuclear12IPCC AR5
Wind offshore12IPCC AR5
Wind onshore11IPCC 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)

PropertyValue
SourceForschungsstelle für Energiewirtschaft (FfE), Munich
DataModeled projection — 8760 hourly values from the Dynamis energy scenario model, based on weather reference year 2012
StorageAzure blob storage (no rate limits): ffeopendatastorage.blob.core.windows.net
LicenseCC-BY-4.0
Year selectionNearest available projection year (2020, 2025, 2030, 2035, 2040, 2045, 2050)
CacheIn-memory, 24-hour TTL (data is static per year)
Methodology stringffe-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).

PatternRange (gCO2/kWh)Cause
Summer midday (10:00–14:00)220–265High solar generation (DE example)
Summer night (00:00–05:00)470–525Fossil baseload (DE example)
Winter (all day)350–435Flatter, wind-dependent (DE example)
Weekend vs. weekday10–20% lowerReduced 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

ParameterValue
Intensity380 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:

ValueBehaviorUse Case
energy-charts (default)Full 3-tier chain: Energy Charts → FfE projection → static tableBest accuracy; requires internet access
ffeLegacy alias — creates the same full 3-tier chain as energy-chartsBackward compatibility
staticStatic 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

ZoneCountryTypical CI (gCO₂eq/kWh)Notes
ATAustria~67Hydro-dominated
BEBelgium~179Gas + nuclear mix
CHSwitzerland~42Hydro + nuclear
DEGermany~258Mixed (coal/gas/wind/solar)
DKDenmark~88Wind-heavy
ESSpain~94Solar + wind + gas
FIFinland~59Nuclear + hydro + biomass
FRFrance~24Nuclear-dominated
ITItaly~206Gas-heavy
NLNetherlands~369Gas-dominated
NONorway~28Hydro-dominated
PLPoland~505Coal-dominated
SESweden~33Hydro + 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:

FamilyGenerationsTDP Range
Intel Xeon PlatinumSkylake, Cascade Lake, Ice Lake, Sapphire Rapids195–350 W
Intel Xeon GoldVarious165–205 W
Intel Xeon SilverVarious100–135 W
Intel Xeon E5Ivy Bridge, Haswell, Broadwell115–145 W
AMD EPYC Rome7000-series225–280 W
AMD EPYC Milan7000-series225–280 W
AMD EPYC Genoa9000-series360 W
AWS GravitonGraviton2, Graviton3, Graviton4130–210 W
AmpereAltra, AmpereOne160–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:

LevelHardware SourceTypical Accuracy
user-providedUser explicitly specified hardwareDepends on user
auto-detectedCPU model matched in TDP database±15–20%
generic-estimateFell 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

LimitationImpact
Statistical approximationPower estimates are modeled, not measured from hardware power meters
CPU-only power modelMemory, storage, network, and GPU power are not modeled separately
Carbon intensity variabilityHourly/15-min data is preferred over annual averages; actual intensity varies by time of day and season
Energy Charts emission factorsIPCC AR5 lifecycle emission factors per fuel type are median values; actual plant-level emissions vary, giving ±10–15% uncertainty
FfE projection dataBased on the Dynamis energy scenario model; projection years (2025, 2030, etc.) may not match actual grid conditions
Graviton/ARM TDP estimatesNot manufacturer specifications; based on ARM Neoverse power characteristics
Uniform PUESingle global default; actual PUE varies by datacenter location, load, and ambient temperature
Limited zone supportCarbon 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 utilizationMean 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
PartMeaning
ccf-linearPower model — Cloud Carbon Footprint linear interpolation (vCPUs × watts)
energy-chartsCarbon intensity source that successfully returned data
DEElectricity grid zone
pue-1.30Power 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.

StandardReference
Green Software Foundation — Software Carbon Intensity (SCI)sci-guide.greensoftware.foundation
Cloud Carbon Footprintcloudcarbonfootprint.org
SPECpower_ssj2008spec.org/power_ssj2008
Green Software Foundation Impact Frameworkif.greensoftware.foundation

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).

EndpointAuth TierOIDC SessionBearer Read Token
GET /healthNone
POST /api/v1/tokenRead tokenYes
POST /api/v1/metricsPush token
GET /api/v1/sizing/*RelaxedYesYes
GET /api/v1/runners/overviewRelaxedYesYes
GET /api/v1/runners/{runner}RelaxedYesYes
All other protected endpointsStrictYesNo

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.

MethodPathDescription
GET/healthHealth check
GET/api/v1/infoService info (version, auth mode, CI provider, forgejo_base_url, logout_url)

Token and Metrics Ingest

MethodPathAuthDescription
POST/api/v1/tokenRead tokenGenerate a scoped push token
POST/api/v1/metricsPush tokenReceive 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

MethodPathDescription
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/metricsDump 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

MethodPathDescription
GET/api/v1/sizing/repo/{org}/{repo}/{workflow}/{job}Compute container sizes from historical data

Query Parameters

ParameterDefaultDescription
runs5Number of recent runs to analyse (1–100)
buffer20CPU headroom percentage (memory uses a staircase buffer)
cpu_percentilep95CPU 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

MethodPathDescription
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.

MethodPathDescription
GET/api/v1/metrics/overviewGlobal 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/reposCross-repo KPI comparison (?org=)
GET/api/v1/compare/workflowsCross-workflow KPI comparison (?org=&repo=)
GET/api/v1/runnersList known runners
GET/api/v1/runners/overviewRunner 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

MethodPathDescription
GET/api/v1/sizing/overridesList 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
DELETESame paths as PUTRemove 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.

MethodPathDescription
GET/ui/loginInitiate OIDC login
GET/ui/callbackOIDC callback
GET/ui/logoutLogout
GET/ui/meCurrent 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.

Backends

The webhook supports two mutation backends:

BackendDescription
KubernetesBackendInline mutation — directly patches the pod spec to add the collector sidecar
EdgeConnectBackendSDK-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:

  1. Map all known containers by process name (e.g., gitlab-runner-helper, collector)
  2. Any remaining cgroup paths that don’t match a known container are assigned to the build container

This is configured via:

CGROUP_STRATEGY=exclusion

Run Metadata

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

VariableDescription
CI_SIZER_RUNNER_NAMEOverride runner name (defaults to pod name)
CGROUP_STRATEGYSet to exclusion for GitLab pods
CI_PROVIDERSet 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

ConditionDescription
STATICFixed resource allocations (baseline — no sizer)
GARM_BAREGARM runner provisioning without sizer
GARM_OBSERVESizer in observe mode (recommendations computed, not enforced)
GARM_ENFORCESizer in enforce mode (recommendations applied as K8s requests/limits)
GARM_WARMSizer enforce mode with pre-warmed historical data

Workloads

WorkloadDescription
carbon-burnerCPU stress workload
memory-stressVariable memory allocation workload
go-buildReal-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

MetricImprovement
CPU oversizing reduction79.64%
Memory oversizing reduction88.59%
Scheduling density improvement12.2× (500m → 41m CPU requests)

Baseline Waste

Without CI Sizer, typical CI workloads exhibit significant resource waste:

WorkloadCPU UtilizationWaste
Batch (carbon-burner)12.8%87%
Go build48.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

ScenarioCompletion Rate
Without sizer (variable-memory workloads)60%
With sizer100%

OOM without the sizer causes node eviction and collateral damage to co-located pods. With the sizer, failures are contained at the cgroup boundary.

Performance Overhead

ModeOverhead
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

Formal KPIs

The benchmark validates the following IPCEI-CIS work package objectives:

KPIWork PackageObjectiveTargetResult
Resource utilizationWP e.1OB 45/46≥10% improvement79–89% improvement
SustainabilityWP e.2OB 47/48≥10% improvement65–90% projected