Production engineering guide for platform cost management covering patterns, implementation strategies, and operational best practices.
Platform cost management is the practice of tracking, analysing, and optimising the financial and operational expenditure of a platform infrastructure across teams, services, and environments. It matters when platforms grow beyond a single team’s budget, when cloud spend becomes a visible and recurring concern, and when engineering leaders need to justify investments in tooling, automation, and platform services.
Cost management begins with visibility. Without accurate, timely, and granular cost data, teams cannot answer: *Who owns what cost? Why did spend spike last week? What’s the cost per developer, per environment, per service?* The first step is collecting cost data from infrastructure, services, and platform components.
Cloud providers expose raw cost data via APIs. AWS offers the GetCostAndUsage API, Azure exposes CostManagement REST endpoints, and GCP provides Billing API with CostByProject and CostByService methods. All return structured JSON with dimensions such as time, service, region, resource, tag, and cost.
A typical cost retrieval request uses curl with authentication via AWS IAM or Azure AD:
curl \
-H "Authorization: Bearer $(cat /secrets/azure-access-token)" \
-H "Content-Type: application/json" \
-d '{
"timePeriod": {
"start": "2024-04-01",
"end": "2024-04-30"
},
"granularity": "DAILY",
"groupings": [
{ "type": "DIMENSION", "keys": ["SERVICE", "REGION"] },
{ "type": "TAG", "keys": ["Environment", "Team"] }
}' \
https://management.azure.com/subscriptions/abc123/resourceGroups/costs/providers/Microsoft.CostManagement/queries/daily-costs?api-version=2023-11-01
The response includes a value array with cost entries:
[
{
"timePeriod": { "start": "2024-04-01", "end": "2024-04-02" },
"cost": 142.37,
"service": "Compute",
"region": "East US",
"tags": {
"Environment": "staging",
"Team": "backend"
}
}
]
This data becomes the foundation for cost allocation. A platform engineer writes a script to extract and normalize cost data daily, storing it in a database. A cost_records table in PostgreSQL receives each entry:
CREATE TABLE cost_records (
id SERIAL PRIMARY KEY,
time_period_start DATE NOT NULL,
time_period_end DATE NOT NULL,
service TEXT NOT NULL,
region TEXT,
environment TEXT,
team TEXT,
cost DECIMAL(12,4) NOT NULL,
source TEXT NOT NULL, -- e.g., "aws", "azure", "gcp"
recorded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Data ingestion happens via a daily cron job:
0 3 * * * postgres /opt/platform/cost-ingest.sh
The script fetches cost data from each provider, normalises it, and inserts into the cost_records table. It logs every request, response, and error:
# cost-ingest.sh
#!/bin/bash
set -euo pipefail
LOG_DIR="/var/log/platform/cost"
DATE=$(date +%Y-%m-%d)
mkdir -p "$LOG_DIR"
exec > "$LOG_DIR/ingest-$DATE.log" 2>&1
echo "Starting cost ingestion for $DATE"
# Fetch from AWS
aws ce get-cost-and-usage \
--time-period Start=2024-04-01,End=2024-04-01 \
--granularity DAILY \
--metrics "UNBLENDED_COST" \
--group-by Type=DIMENSION,Keys=["SERVICE","REGION"] \
--output json \
--query 'ResultsByTime[].{TimePeriod: TimePeriod, Cost: Total.UnblendedCost.Amount}' \
| jq -r '
.[] |
.TimePeriod |
{ start: .Start, end: .End } |
.Cost |
{ amount: .Amount, unit: .Unit } |
. |
. + { source: "aws" } |
[ .start, .end, .amount, .unit, .source ] |
@tsv
' \
| psql -c "COPY cost_records(time_period_start, time_period_end, cost, source) FROM STDIN"
# Fetch from Azure, GCP, etc.
# ...
This daily pipeline is not silent. A failure in curl or jq results in incomplete or malformed data. A missing time_period_start column or cost value leads to gaps in reporting. A 503 Service Unavailable from Azure’s CostManagement API triggers an alert via systemctl:
# /etc/systemd/system/cost-ingest-alert.service
[Unit]
Description=Cost Ingest Alert
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/platform/alert-on-failure.sh
Environment=LOG_FILE=/var/log/platform/cost/alerts.log
Restart=on-failure
RestartSec=300
[Install]
WantedBy=multi-user.target
Once data is collected, costs must be assigned to logical units: teams, services, environments, or feature areas. The most common method uses tags and labels applied at infrastructure and service creation time.
A service deployed via Kubernetes uses kubernetes.io/cluster and app.kubernetes.io/instance labels. A team tags their GKE cluster with team=backend, environment=production, and owner=john-doe. A CI pipeline adds project=order-service to every pod.
These tags are used to group and allocate costs. A cost allocation job runs nightly:
# /opt/platform/cost-allocation.sh
#!/bin/bash
set -euo pipefail
psql << 'EOF' | psql -c 'COPY cost_allocation FROM STDIN'
SELECT
c.team,
c.environment,
c.service,
SUM(c.cost) AS total_cost,
COUNT(*) AS record_count,
AVG(c.cost) AS avg_daily_cost
FROM cost_records c
GROUP BY c.team, c.environment, c.service
ORDER BY total_cost DESC
EOF
The result is a cost_allocation table:
CREATE TABLE cost_allocation (
team TEXT,
environment TEXT,
service TEXT,
total_cost DECIMAL(12,4),
record_count INT,
avg_daily_cost DECIMAL(12,4)
);
This table powers cost dashboards, alerts, and budgeting. A team can see their total platform spend and drill down into services. A service can compare its cost across environments.
The biggest failure mode in allocation is tag misapplication. A developer creates a new service but forgets to add team=frontend. The service appears in cost reports under team=null, or is lumped into a default team=platform. A missing environment=staging tag results in staging costs being attributed to production.
Another silent failure: costs not aligned to time periods. A service is deployed mid-month, but its cost is attributed from the first day of the month. A cost_records entry with time_period_start=2024-04-15 and time_period_end=2024-04-16 is not properly prorated. A cost_allocation query that groups by time_period_start but not time_period_end undercounts mid-month costs.
Cost management is not passive. Teams must be held accountable. Cost governance is enforced via budgets, alerts, and policies.
A budget is a target spend for a team, service, or environment. It is defined in a budgets table:
CREATE TABLE budgets (
id SERIAL PRIMARY KEY,
team TEXT,
environment TEXT,
service TEXT,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
target_amount DECIMAL(12,4) NOT NULL,
current_amount DECIMAL(12,4) DEFAULT 0,
status TEXT CHECK (status IN ('active', 'overbudget', 'warning', 'expired')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
A daily job checks current spend against target:
# /opt/platform/cost-budget-check.sh
#!/bin/bash
set -euo pipefail
DATE=$(date +%Y-%m-%d)
# Update current amounts
psql -c "
UPDATE budgets
SET current_amount = (
SELECT COALESCE(SUM(cost), 0)
FROM cost_records c
WHERE c.time_period_start <= budgets.period_end
AND c.time_period_start >= budgets.period_start
AND c.team = budgets.team
AND (budgets.environment IS NULL OR c.environment = budgets.environment)
AND (budgets.service IS NULL OR c.service = budgets.service)
)
WHERE period_end >= '$DATE'
AND status IN ('active', 'warning');
"
# Check thresholds
psql -c "
SELECT
b.team,
b.environment,
b.service,
b.target_amount,
b.current_amount,
(b.target_amount - b.current_amount) AS remaining,
CASE
WHEN b.current_amount >= b.target_amount THEN 'overbudget'
WHEN b.current_amount >= b.target_amount * 0.9 THEN 'warning'
ELSE 'active'
END AS new_status
FROM budgets b
WHERE b.period_end >= '$DATE'
AND b.status IN ('active', 'warning')
AND b.current_amount < b.target_amount
ORDER BY b.current_amount
"
When a budget is over budget, an alert is sent. A cost-budget-alert.service runs every 5 minutes:
# /etc/systemd/system/cost-budget-alert.service
[Unit]
Description=Cost Budget Alert
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/platform/send-budget-alerts.sh
Environment=SLACK_WEBHOOK=https://hooks.slack.com/services/abc123
Restart=on-failure
RestartSec=300
[Install]
WantedBy=multi-user.target
The alert script queries for overbudget items and sends a message to Slack:
# send-budget-alerts.sh
#!/bin/bash
set -euo pipefail
SLACK_WEBHOOK="$SLACK_WEBHOOK"
# Query overbudget budgets
psql -c "
SELECT
team,
COALESCE(environment, 'all'),
COALESCE(service, 'all'),
target_amount,
current_amount,
(current_amount - target_amount) AS overage
FROM budgets
WHERE status = 'active'
AND current_amount >= target_amount
ORDER BY overage DESC
" | jq -r '
.[] |
{
text: "*Cost Overbudget*",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "*Team:* \(.team)\n*Environment:* \(.environment)\n*Service:* \(.service)\n*Target:* \($.target_amount | round(2))\n*Current:* \($.current_amount | round(2))\n*Overage:* \($.overage | round(2))"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: "*Status:* Overbudget"
},
{
type: "mrkdwn",
text: "*Threshold:* 100%"
}
]
}
]
} |
@json
' | curl -X POST -H "Content-Type: application/json" -d @- "$SLACK_WEBHOOK"
A common failure: budgets not updated after deployment. A new service is deployed in May, but the budget for that service starts on June 1. April’s spend is not attributed to it. The period_start and period_end columns are misaligned. A budgets row with period_start=2024-04-01, period_end=2024-04-30, but the service’s first cost record is time_period_start=2024-04-15 — the cost is not fully captured.
Cost management becomes cost optimisation when teams act on insights. The most effective pattern is cost per unit — cost per developer, per deployment, per feature, per environment.
A team calculates cost per developer by aggregating total platform spend and dividing by number of developers:
SELECT
SUM(t.total_cost) AS total_platform_cost,
COUNT(DISTINCT d.developer_id) AS developers,
(SUM(t.total_cost) / COUNT(DISTINCT d.developer_id)) AS cost_per_developer
FROM cost_allocation t
JOIN developers d ON t.team = d.team
WHERE t.environment = 'development'
AND t.service IN ('backend', 'frontend', 'api-gateway')
GROUP BY t.team;
A high cost per developer triggers action: reduce environment retention, upgrade shared services, or automate provisioning.
The most impactful optimisation is environment lifecycle management. A platform defines three environments: development, staging, and production. Each has a cost profile.
A environments table tracks the state of each environment:
CREATE TABLE environments (
id SERIAL PRIMARY KEY,
team TEXT,
service TEXT,
environment TEXT,
created_at TIMESTAMP WITH TIME ZONE,
last_active TIMESTAMP WITH TIME ZONE,
status TEXT CHECK (status IN ('active', 'idle', 'suspended', 'deleted')),
cost_per_day DECIMAL(12,4),
active_days INT
);
A nightly job identifies idle environments and suspends them:
# /opt/platform/suspend-idle-environments.sh
#!/bin/bash
set -euo pipefail
IDLE_DAYS=30
COST_THRESHOLD=50.00
psql -c "
UPDATE environments
SET status = 'suspended'
WHERE status = 'active'
AND last_active < NOW() - INTERVAL '$IDLE_DAYS days'
AND cost_per_day > $COST_THRESHOLD
AND id NOT IN (
SELECT id FROM environments WHERE environment = 'production'
)
RETURNING id, environment, team, last_active, cost_per_day
"
The script logs every suspended environment and sends a notification:
echo "Suspended idle environments:" > /var/log/platform/autopause.log
psql -c "
SELECT
id,
environment,
team,
last_active,
cost_per_day
FROM environments
WHERE status = 'suspended'
AND updated_at > NOW() - INTERVAL '1 day'
ORDER BY last_active
" | jq -r '
.[] |
"ID: \(.id) | \(.team)/\(.environment) | Last active: \(.last_active) | Cost/day: \(.cost_per_day)"
' >> /var/log/platform/autopause.log
The sharp edge: suspended environments are not automatically resumed. A developer deploys a feature to staging, but the environment is suspended. They must manually resume it, or the platform must define a resume-on-deploy policy: on every git push to staging, check if the environment is suspended; if so, trigger a resume-environment job.
A resume-environment.service monitors for new deployments:
[Unit]
Description=Resume Environment on Deploy
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/platform/resume-environment.sh
Environment=KUBECONFIG=/etc/kubernetes/admin.conf
Restart=on-failure
RestartSec=300
[Install]
WantedBy=multi-user.target
This job uses kubectl to check for new commits and resume environments:
# resume-environment.sh
#!/bin/bash
set -euo pipefail
COMMIT_FILE="/tmp/latest-commit.txt"
KUBECONFIG="$KUBECONFIG"
# Get latest commit from Git
git log --oneline -1 > "$COMMIT_FILE"
CURRENT_COMMIT=$(cat "$COMMIT_FILE")
# Check if environment is suspended
if kubectl get env -l environment=staging -o jsonpath='{.items[0].status}' | jq -e '.status == "suspended"'; then
kubectl patch environment staging -p '{"status": "active"}'
echo "Resumed staging environment"
fi
The system silently fails when the kubectl command is missing, or the env custom resource is not defined. A 404 Not Found from the kube-apiserver during patch results in an environment that remains suspended despite a new deployment.
Platform cost management is not about reporting. It is about visibility, allocation, governance, and optimisation. The core pattern is: collect raw cost data, allocate it using labels and tags, enforce budgets, and act on insights. The most common failure is misaligned time periods and tag drift. The most valuable insight is cost per unit. The most effective practice is automated environment lifecycle management. A platform that manages cost well is one where teams understand their spend, where decisions are data-driven, and where platforms evolve to reduce cost over time.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy