What the cost of microservices is made of: three buckets

When engineers estimate the cost of microservices, they usually start with the cloud bill. That is reasonable, because the bill is the number someone already sends you each month. It is also the smallest part of the picture. A useful cost model splits spending into three buckets.

Direct infrastructure

This bucket covers compute, networking, and storage. It includes container or VM capacity, load balancers, cross-zone and egress traffic, managed databases, and log and metric retention. It is the easiest bucket to measure because billing exports already tag it by account, cluster, or label. It also tends to be the smallest. Hardware is cheap compared to engineer time, and a service that idles at low utilization still costs little in absolute terms.

Per-service overhead

This is the recurring human work that every service brings with it, whatever it does. Each service needs a CI/CD pipeline, dashboards, and alerts. Its dependencies need upgrading, its images and libraries need security patches, and it widens the on-call surface. None of these tasks is large. They grow linearly with service count, though, and they rarely appear in any budget line. That combination is why this bucket is usually the biggest and the most often underestimated.

Fixed platform cost

This is the platform team plus the shared tooling it runs: the deployment system, the observability stack, service templates, and the internal developer portal. You pay it once. Its job is to make per-service overhead smaller.

This analysis is for backend and platform engineers who have a concrete decision in front of them: splitting a service, consolidating several, or deciding whether a platform investment pays for itself.

                  TOTAL COST OF MICROSERVICES
                               |
       +-----------------------+------------------------+
       v                       v                        v
+--------------+     +------------------+     +------------------+
| DIRECT INFRA |     | PER-SERVICE      |     | FIXED PLATFORM   |
|              |     | OVERHEAD         |     | COST             |
|              |     |                  |     |                  |
| compute      |     | CI/CD pipeline   |     | platform team    |
| networking   |     | dashboards       |     | deploy system    |
| storage      |     | alerts           |     | observability    |
|              |     | dep upgrades     |     | templates        |
|              |     | security patches |     | dev portal       |
|              |     | on-call surface  |     |                  |
+--------------+     +------------------+     +------------------+
| measured via |     | x N services     |     | paid once        |
| billing data |     | linear growth    |     |                  |
|              |     | rarely budgeted  |     |                  |
| usually      |     | usually largest  |     |                  |
| smallest     |     |                  |     |                  |
+--------------+     +------------------+     +------------------+
                               ^                        |
                               |    reduces overhead    |
                               +------------------------+

The three cost buckets of microservices: direct infrastructure is easy to measure and usually smallest, per-service overhead grows with every service and usually dominates, and fixed platform cost is paid once so that per-service overhead gets smaller.

How the cost model works: total = platform + (services × overhead) + infrastructure

Every term is expressed per month so the pieces add cleanly:

Total = P + (N × h × r) + I

  • P is the fixed platform cost: shared CI runners, cluster control planes, artifact storage, and the salaries of any dedicated platform engineers. It does not change when you add one service.
  • N × h × r is the per-service overhead. N is the service count, h the engineer-hours each service consumes monthly (dependency bumps, pipeline fixes, on-call, alert tuning), and r the loaded hourly cost.
  • I is infrastructure, summed per service: baseline replicas × resource requests × unit price, plus observability ingestion (GB/month × price per GB). Use requests rather than actual usage, because requests are what reserve capacity.

In a spreadsheet: =P + N*h*r + SUMPRODUCT(replicas, cpu_req, cpu_price) + SUMPRODUCT(replicas, mem_req, mem_price) + log_gb*gb_price.

Why h is not a constant

Treat h as a function of N. Shared templates, a single deploy path, and uniform logging let each new service inherit fixes, so h flattens or falls. Every extra language, build tool, or bespoke config adds a context switch nobody amortizes, so h rises with N.

The platform team as a step

Staffing a team bumps P by a whole team's cost at once, and in exchange it lowers h. It pays off when N × (h_before − h_after) × r > team cost.

Illustrative estimate (assumed numbers, not a benchmark): three engineers at $20,000/month loaded cost $60,000. If h drops from 20 to 8 hours at r = $100, the crossover is 60,000 ÷ (12 × 100) = 50 services.

# Illustrative estimate: Total = P + N*h*r + I, all per month, assumed inputs only
R = 100.0                  # loaded cost per engineer-hour, USD
BASE_P = 8_000.0           # shared CI runners, control planes, artifact storage
TEAM_COST = 3 * 20_000.0   # step added to P when a platform team is staffed
H_BEFORE, H_AFTER = 20.0, 8.0  # hours/service/month without and with the team

def h_of_n(n, base_h, drift):
    # drift > 0: each extra language or bespoke config makes services costlier as N grows
    # drift < 0: shared templates let new services inherit fixes, so h falls
    return max(base_h * (1 + drift * n), 1.0)

def infra(n, replicas=2, cpu_req=0.5, cpu_price=25.0,
          mem_gb=1.0, mem_price=3.0, log_gb=20.0, gb_price=0.5):
    # Price requests, not usage: requests are what reserve capacity
    per_svc = replicas * (cpu_req * cpu_price + mem_gb * mem_price) + log_gb * gb_price
    return n * per_svc

def total(n, with_team, drift=0.0):
    p = BASE_P + (TEAM_COST if with_team else 0.0)
    h = h_of_n(n, H_AFTER if with_team else H_BEFORE, drift)
    return p + n * h * R + infra(n)

# Linear crossover: N * (h_before - h_after) * r > team cost
print("break-even N:", TEAM_COST / ((H_BEFORE - H_AFTER) * R))  # 50.0

print("N   no_team  no_team_heterogeneous  with_team")
for n in (10, 30, 50, 80):
    print(n,
          round(total(n, with_team=False)),
          round(total(n, with_team=False, drift=0.01)),  # h climbs with N
          round(total(n, with_team=True)))

A short Python script that reproduces the cost model with assumed inputs. It treats h as a function of N and adds the platform team as a step in P, so the break-even works out to 50 services when h is linear and arrives sooner when a heterogeneous stack makes h rise.

A worked example: 40 services with assumed inputs

All figures below are illustrative estimates built from assumed inputs, not measurements.

Assumed inputs: 40 services, a loaded engineer rate of r = $100/hour, two minimum replicas per service at $50 each per month, and $50 of telemetry (logs, metrics, traces) per service per month. The platform team is three engineers at $60,000/month. Only h, the maintenance hours per service per month, varies between cases. It covers dependency bumps, pipeline fixes, alert tuning and on-call follow-ups.

Case h (hours/service/month) Per-service cost 40 services + platform team
Low 4 $550 $82,000
Expected 8 $950 $98,000
High 20 $2,150 $146,000

Infrastructure is the same $150 per service in every row. The spread comes almost entirely from h. Each extra hour per service adds $4,000/month at 40 services. Teams tend to underestimate h because the work is spread across many people in small amounts and nobody logs it, so plan around the expected or high row rather than the low one.

Service count changes the comparison. Without a platform team, assume h = 20. Forty services then cost $86,000/month, which is less than the $98,000 expected case with a team. Below the crossover of 50 services, the platform team does not pay for itself on direct cost. At 60 services the order flips: $129,000 without a team against $117,000 with one. The platform team is a fixed step cost. It makes financial sense only once the hours it saves across all services exceed its fixed price.

# Illustrative estimate only: reproduces the section's cost model from assumed inputs.
RATE = 100              # assumed loaded engineer cost, $/hour
INFRA = 2 * 50 + 50     # 2 min replicas at $50 + $50 telemetry, $/service/month
PLATFORM_TEAM = 60_000  # assumed 3 engineers, fixed $/month
H_WITH_TEAM = 8         # expected maintenance hours/service/month with a platform team
H_WITHOUT_TEAM = 20     # assumed hours/service/month when every team maintains its own

def per_service(h):
    return h * RATE + INFRA

def total(n, h, platform=True):
    return n * per_service(h) + (PLATFORM_TEAM if platform else 0)

# Sensitivity on h at 40 services: the input teams tend to underestimate.
for case, h in [("low", 4), ("expected", 8), ("high", 20)]:
    print(f"{case:>8}: h={h:>2}  per-service=${per_service(h):,}  total=${total(40, h):,}")

# Each extra hour per service per month at 40 services.
print(f"marginal hour: ${40 * RATE:,}/month")

# Where the platform-team step pays for itself:
# n * per_service(20) = n * per_service(8) + PLATFORM_TEAM
saving = per_service(H_WITHOUT_TEAM) - per_service(H_WITH_TEAM)
print(f"crossover: {PLATFORM_TEAM / saving:.0f} services")

for n in (20, 40, 50, 60, 80):
    without = total(n, H_WITHOUT_TEAM, platform=False)
    with_team = total(n, H_WITH_TEAM)
    cheaper = "team" if with_team < without else "no team" if without < with_team else "equal"
    print(f"n={n:>3}  no team=${without:,}  with team=${with_team:,}  cheaper: {cheaper}")

A small Python model that reproduces the section's illustrative estimates, including the low/expected/high totals at 40 services, the $4,000 cost of each extra maintenance hour, and the 50-service crossover where the fixed platform-team cost starts to pay for itself.

Where per-service overhead hides and how to measure it

The overhead term in the model is the one teams usually guess at, because most of it never shows up as a line item. It is spread across six places, and each one already has a data source you can read.

  • Idle baseline replicas. Each service keeps two or three replicas running at 3 a.m. for availability, whether or not it gets traffic. Pull compute spend per service from billing tags or orchestrator labels and compare it with request volume.
  • Over-provisioned requests. Schedulers reserve what a service declares, not what it uses. Compare declared requests with observed usage, for example kubectl top pods (which needs a metrics server) against the pod spec. The gap is capacity you pay for and never use.
  • Telemetry volume. Every service emits its own logs, metrics, and traces, and observability pricing usually scales with ingest. Break ingest down by service label.
  • Cross-service traffic. Each network hop adds latency and, across zones, transfer charges. Use flow logs or mesh metrics to see which service pairs talk to each other most.
  • Duplicated upgrades. A base-image or library patch is repeated in every repository. CI minutes per repository show the build cost, and alert and page counts per service show the operational load.
  • Cross-service coordination. A change that touches four services needs four reviews and an ordered deploy. Ticket links and time-tracking data show how often this happens.

Treat these numbers as lower bounds. Shared clusters, shared databases, and untagged resources make per-service attribution unreliable. When a node pool or database is shared, its cost gets split by convention, not by measurement, so record the allocation rule you use and do not read the result as exact.

#!/usr/bin/env bash
# Compare declared resource requests with observed usage for one service.
# Needs metrics-server in the cluster so that `kubectl top` returns data.
set -euo pipefail

NS="${1:-payments}"          # namespace that holds the service
SELECTOR="${2:-app=checkout}"  # label that identifies the service's pods
export LC_ALL=C                # sort and join must use the same collation

req=$(mktemp); use=$(mktemp)
trap 'rm -f "$req" "$use"' EXIT

# Declared requests: what the scheduler reserves, and what you pay for.
# Pods with several containers show comma-separated values per column.
kubectl get pods -n "$NS" -l "$SELECTOR" --no-headers \
  -o custom-columns='POD:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory' \
  | sort -k1,1 > "$req"

# Observed usage: a point-in-time sample, not a peak or an average.
kubectl top pods -n "$NS" -l "$SELECTOR" --no-headers \
  | sort -k1,1 > "$use"

# Put requests and usage side by side. The gap between the columns is
# capacity you reserve and pay for but never use.
{
  echo "POD CPU_REQ MEM_REQ CPU_USED MEM_USED"
  join -j 1 "$req" "$use"
} | column -t

# A single sample understates peaks. Run this on a schedule, or read the
# same series from your metrics store, before you resize anything.

A shell script that lines up each pod's declared CPU and memory requests with a kubectl top usage sample for one service, so you can see how much reserved capacity goes unused.

Alternatives: modeling the monolith and modular monolith on the same terms

For a fair comparison, apply the same three buckets to the alternatives instead of setting them to zero. A monolith still needs a platform: CI, deployment, observability and on-call tooling. It has a single service, so the per-service overhead term is small. That overhead doesn't disappear, though. It shows up as other costs.

Cost Microservices Modular monolith Monolith
Build and test time Small per service, many pipelines Grows with the codebase; module boundaries allow selective testing Grows with the codebase; often runs the full suite
Blast radius of a bad deploy One service, plus its callers Whole process Whole process
Team contention Low in code, high at API contracts Moderate; boundaries reduce merge conflicts High; shared files and release queues
Release coordination Needed only for breaking contract changes Single release train Single release train

These costs go on the same ledger. A 40-minute test suite run by 60 engineers several times a day is a labor cost, just as a per-service dashboard is. A release train that waits on the slowest team delays every change behind it.

Reliability cuts both ways. Each network hop adds timeouts, retries, partial failures and traces that span several services, so incidents take longer to diagnose. A monolith avoids those failure modes but has a single failure domain: a memory leak in one module can take down checkout and search together. Neither option is automatically cheaper on call. Model incident frequency and incident size separately.

The steady-state formula leaves out migration. Splitting a monolith or consolidating services is a one-time cost in engineering time, dual-running infrastructure and risk. Amortize it over a realistic horizon before you compare annual totals.

Cutting per-service overhead without changing architecture

Most of the per-service constant is repeated work, so the cheapest savings come from doing that work once.

Shared templates and golden paths. A service scaffold that already includes CI, health checks, logging and deployment manifests turns setup and drift into a one-time platform cost. It also lowers ongoing overhead: when every service looks alike, on-call engineers spend less time learning each one.

Centralized update automation. One bot that opens dependency and base-image pull requests across all repositories, with shared test gates, replaces forty teams doing the same chore on forty schedules. The cost per update moves from engineering hours toward review minutes.

Telemetry sampling and retention. Trace and log volume grows with the number of services, and so does its cost. Head-based sampling, such as setting OTEL_TRACES_SAMPLER=parentbased_traceidratio with a ratio of 0.1, combined with short retention for debug logs, reduces the per-service infrastructure term directly.

Right-sizing requests. Requests are often copied from a template and never revisited. Reset CPU and memory requests to observed p95 usage plus headroom, for example lowering a 500m CPU request to 200m. This reclaims capacity that every replica was reserving.

Consolidating dormant services. A service that changes a few times a year still carries the full overhead. Merging it into a neighbor removes that overhead.

The adoption risk. If tooling goes unused, it adds to the platform term and takes nothing off the per-service constant. Track the share of services on the current template, the median base-image age, and how often teams route around the platform. If those numbers stay flat, stop building and find out why.

When to add a service, when to avoid one, and how to decide

Before adding a service, estimate its monthly cost from the model: the per-service overhead constant plus whatever infrastructure it needs on its own. Then ask what that buys. A new service can provide team autonomy, independent deployment, fault isolation, or targeted scaling. If none of those is worth more than the monthly figure, keep the code in an existing deployable.

Signal Favors splitting Favors staying together
Team structure Several teams blocked waiting on one deployable's release train One small team owns the code
Change rate Frequent, independent changes to one area Low change rate; releases are rare anyway
Scaling profile One component needs very different CPU, memory, or replica counts Load is roughly uniform across the codebase
Failure impact A fault in one area should not take down the rest Failures are already contained or tolerable
Platform tooling Templates, CI, and observability are in place Each service would be hand-built

The table covers only half the comparison. Costs are easy to price because they show up as hours and invoices. Benefits are hard to price. What is unblocking a team worth per month? What is avoiding a cross-domain outage worth? Because of this asymmetry, a cost-only model will tend to recommend consolidation even when the organization has grown enough to justify a split. Treat a model that says "merge" as a prompt to check what you would lose, not as a verdict.

The inputs also change over time. As team count grows, coordination costs rise inside a shared deployable. As service count grows, platform investment spreads across more services and per-service overhead can fall. Rerun the model whenever either count changes meaningfully, and at least once a year.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to cost of microservices, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Changing a shared variable is one edit and one review instead of editing every service's .env by hand.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.