What drives monolith vs microservices cost
Total cost of ownership splits into three buckets that you can measure: infrastructure cost, operational overhead and team cost. Most of the cost gap between a monolith and microservices comes from fixed per-service overhead. Each new service needs its own deployment pipeline, dashboards, alerts, on-call runbook, dependency upgrades and network boundary. That overhead grows with the number of services, not with traffic. Doubling requests per second barely changes it. Doubling the number of services roughly doubles it.
| Bucket | What it includes | Monolith | Microservices |
|---|---|---|---|
| Infrastructure cost | Compute, storage, network, managed services | One deployable, scaled as a unit; in-process calls | Per-service baseline capacity; cross-service network traffic; often a database per service |
| Operational overhead | CI/CD, observability, incident response, security patching | One pipeline, one set of dependencies to patch | N pipelines, distributed tracing, N dependency trees to patch |
| Team cost | Coordination, platform work, cognitive load | Shared release coordination, merge contention | API contracts, platform team, reasoning about partial failures |
Teams accept this overhead for two reasons:
- Independent scaling. You can add replicas or different hardware for the hot path only. This saves money only when load is uneven across components. If everything scales together, the savings are close to zero.
- Independent delivery. Teams ship without waiting for a shared release. This saves money only when release coordination is a real bottleneck, which usually means several teams working on one codebase.
The break-even condition is:
scaling savings + delivery savings > (N × per-service overhead) + cross-service costs
If the left side doesn't clearly exceed the right side for your system, the extra services cost more than they save.
How per-service fixed costs multiply
The right side of that inequality grows faster than most teams expect. Every service has a cost floor before it handles a single request. It needs two or three replicas for availability, a load balancer or ingress rules, a CI pipeline, dashboards and alerts, secrets to rotate, dependency updates to merge, and often its own database. A simple model:
Fixed cost ≈ N × (R × replica cost + ingress + DB + CI + observability + maintenance hours × hourly rate)
Illustrative estimate (assumed numbers, not a benchmark): assume $250/month of baseline infrastructure per service, plus 4 maintenance hours at $100/hour. Each service then costs $650/month while idle. Twenty services cost $13,000/month before any traffic arrives.
Network costs are easy to underestimate:
- Serialization: In a monolith, passing data is a function call. Across services, every hop has to encode and decode the payload, which uses CPU.
- Sidecars and service mesh: A sidecar reserves CPU and memory on every pod, not just the busy ones.
- Cross-zone transfer: On most clouds, chatty calls between availability zones are billed per GB.
- Tail latency: A request has to wait for its slowest dependency, so slow responses add up across the call chain. Teams then overprovision to keep p99 latency within budget.
Observability costs also grow with fan-out. One user request now produces many spans and log lines, and per-service labels increase metric cardinality. Both drive up ingestion and retention charges.
Monolith: client -> LB -> [app: A() -> B() -> C()] -> DB
Services: client -> LB -> [sidecar|A] --zone a->b--> [sidecar|B] -> DB-B
|
+--zone a->c--> [sidecar|C] -> DB-C
each arrow: serialize + mesh hop + spans + logs (+ $ per GB cross-zone)
# Illustrative estimate: fixed per-service cost floor plus cross-zone transfer.
# All inputs are assumptions. Replace them with your own bill and rates.
REPLICAS = 3 # minimum replicas kept for availability
REPLICA_COST = 40.0 # $/month per replica (assumed)
INGRESS = 20.0 # $/month for LB or ingress share (assumed)
DB = 60.0 # $/month for a small per-service database (assumed)
CI = 15.0 # $/month of CI runner time (assumed)
OBSERVABILITY = 35.0 # $/month for logs, metrics, traces ingestion (assumed)
MAINT_HOURS = 4 # engineer hours/month: upgrades, alerts, secrets
HOURLY_RATE = 100.0 # $/hour, loaded engineering cost (assumed)
CROSS_AZ_PER_GB = 0.02 # $/GB for traffic between zones (assumed; check your provider)
def fixed_cost(n_services: int) -> float:
"""Cost floor paid before any request arrives."""
per_service = (REPLICAS * REPLICA_COST + INGRESS + DB + CI
+ OBSERVABILITY + MAINT_HOURS * HOURLY_RATE)
return n_services * per_service
def cross_az_cost(requests_per_month: int, hops: int, kb_per_hop: float,
cross_zone_fraction: float) -> float:
"""Transfer cost of chatty calls; a monolith pays ~0 for in-process hops."""
gb = requests_per_month * hops * kb_per_hop * cross_zone_fraction / 1_000_000
return gb * CROSS_AZ_PER_GB
for n in (1, 5, 20, 50):
print(f"{n:>3} services: fixed floor ${fixed_cost(n):>9,.0f}/month")
# 100M requests, 6 hops each, 20 KB per hop, ~2/3 of hops cross a zone boundary
print(f"cross-AZ transfer: ${cross_az_cost(100_000_000, 6, 20, 0.67):,.0f}/month")
A small Python model (illustrative estimate, assumed prices) showing that the fixed cost floor grows linearly with the number of services, and how chatty cross-zone calls add a separate transfer cost that a monolith largely avoids.
Where microservices save infrastructure money
That overhead only pays for itself when components behave differently under load. A monolith scales horizontally as one unit. If search needs four times the CPU at peak, every new replica also carries the admin UI, the report generator and checkout, and each replica is sized for the hungriest code path.
Splitting recovers money in three ways:
- Hot paths scale alone. Only the search service adds replicas. The rest stays at its baseline.
- Instance shape matches the workload. A memory-heavy cache, a CPU-bound encoder and an I/O-bound API each run on hardware suited to them. In a monolith, all three share one shape.
- Batch stays away from latency-sensitive work. A nightly export can run on cheap interruptible capacity. The request path no longer has to keep headroom for it.
| Dimension | Monolith | Microservices |
|---|---|---|
| Scaling unit | Whole application | Individual service |
| Instance shape | One shape for all code | Per-workload |
| Batch vs. request isolation | Shared replicas | Separate pools |
| Idle cost on serverless | N/A | Near zero |
Illustrative estimate (assumed numbers, not a benchmark). Savings = (R_mono × C_mono) − Σ(rᵢ × cᵢ) − O, where O is the per-service overhead from the previous section. Assume the monolith needs 12 replicas at $200 each, which is $2,400. A split runs search at 10 × $150 plus three other services at 2 × $100 each, which is $2,100. If O is $250, the net saving is $50.
If load is uniform across components, Σ(rᵢ × cᵢ) roughly equals R_mono × C_mono, and O becomes pure loss.
Managed and serverless platforms turn fixed per-service costs into usage-based ones. That cuts idle spend. It also means the bill grows with every request, which makes it harder to predict at high traffic.
Impact on development cost and engineering time
Infrastructure is usually the smaller bill. Salaries dominate, so the architecture that wastes fewer engineering hours tends to cost less overall.
| Cost driver | Monolith | Microservices |
|---|---|---|
| Coordination | Merge contention, shared release train | API contracts, versioning, deprecation windows |
| Feedback loop | Builds and test suites slow as the codebase grows | Fast per-service builds, slower integration testing |
| Failure scope | One bad change blocks or breaks every deploy | Partial failures, cross-service debugging |
| Data consistency | Database transactions | Sagas, outbox patterns, reconciliation jobs |
| Fixed headcount | Minimal | Platform team, more on-call rotations |
Illustrative estimate (assumed numbers, not a benchmark). Assume 60 product engineers.
Overhead FTE = platform FTE + (engineers × share of time lost to coordination)
- Microservices: 3–6 platform FTE, plus 10–20% of time spent on contracts, coordinated releases, extra on-call, and consistency work. That gives 3–6 + 6–12 = 9–18 FTE.
- Monolith: 0–1 FTE for build tooling, plus 10–25% of time lost to merge conflicts, waiting on CI, and rollbacks after shared deploys. That gives 6–16 FTE.
The ranges overlap, and the percentages decide the result. Whoever picks them can tilt the outcome either way, so measure your own numbers before trusting either figure.
Microservices save engineering time when domain boundaries are stable and teams can ship without waiting on each other. A monolith saves time when the team is small enough that one transaction replaces a saga and one deploy replaces a coordinated release. As headcount grows, that advantage shrinks, because more people contend for the same build pipeline and the same release window.
A worked break-even model (illustrative estimate)
Contention is what the model has to price. Treat every service as a fixed monthly charge, and set that against the savings that only appear once components scale and deploy independently.
Illustrative estimate (assumed numbers, not a benchmark). Assumed inputs:
- 8 engineers at $100/hour.
- 2 minimum replicas per service at $40 each.
- $30 per service for the pipeline and $50 per service for observability.
- 4 maintenance hours per service per month.
- Monolith compute of $6,000/month.
Per-service overhead = R × C_replica + P + O + H × rate = 2×40 + 30 + 50 + 4×100 = $560/service/month. Engineering hours dominate the overhead, not the cloud bill.
Monthly savings = (Compute_mono × u) + (engineers × deploy-hours saved × rate) + (incidents × engineer-hours per incident × blast-radius reduction × rate).
- u is the share of monolith compute wasted by scaling everything together.
- Baseline: 6,000×0.4 + 8×3×100 + 2×10×0.5×100 = $5,800.
Break-even services = savings ÷ per-service overhead.
| Scenario | Changed input | Savings/month | Break-even services |
|---|---|---|---|
| Baseline | none | $5,800 | ≈10 |
| Even load | u = 0.1 | $4,000 | ≈7 |
| Costly incidents | incident term = $3,000 | $7,800 | ≈13 |
| Small team | 4 engineers | $4,600 | ≈8 |
Below the break-even count, splitting pays for itself. Past it, each new service costs more than it saves.
The model assumes savings stay flat as you add services, but they don't. Once the hot path is isolated, further splits recover little additional compute. That is why the useful break-even is usually lower than the formula suggests.
Uneven load and expensive incidents raise the ceiling. A small team, or load that is evenly spread, lowers it.
# Illustrative estimate: placeholder inputs, not measured data. Replace with your own.
import math
BASE = dict(
engineers=8, rate=100, # team size, $/engineer-hour
replicas=2, replica_cost=40, # min replicas per service, $/replica/month
pipeline=30, observability=50, # $/service/month
maint_hours=4, # engineer-hours per service per month
mono_compute=6000, u=0.4, # monolith $/month, share wasted by scaling together
deploy_hours_saved=3, # per engineer per month
incidents=2, hours_per_incident=10, blast_reduction=0.5,
)
def overhead(p):
# Fixed monthly cost each extra service adds; engineering time usually dominates
return (p["replicas"] * p["replica_cost"] + p["pipeline"]
+ p["observability"] + p["maint_hours"] * p["rate"])
def savings(p):
# Assumed flat in service count; real savings flatten once the hot path is split out
compute = p["mono_compute"] * p["u"]
deploys = p["engineers"] * p["deploy_hours_saved"] * p["rate"]
incident = (p["incidents"] * p["hours_per_incident"]
* p["blast_reduction"] * p["rate"])
return compute + deploys + incident
SCENARIOS = {
"Baseline": {},
"Even load": {"u": 0.1},
"Costly incidents": {"hours_per_incident": 30}, # incident term = $3,000
"Small team": {"engineers": 4},
}
for name, change in SCENARIOS.items():
p = {**BASE, **change}
s, o = savings(p), overhead(p)
# Largest whole number of services whose total overhead the savings still cover
print(f"{name:<17} overhead=${o:,.0f}/svc savings=${s:,.0f} "
f"break-even={math.floor(s / o)} services")
A rerunnable Python version of the illustrative break-even model. It computes per-service overhead ($560) and monthly savings, then prints the break-even service count for each scenario in the table (10, 7, 13, 8).
Who should use microservices, when to choose them and when to avoid them
When microservices pay off
Microservices fit organizations that already have several teams, each owning a clear business domain end to end. The architecture's main return is coordination: teams deploy, scale, and fail independently instead of queuing behind a shared release. Two other conditions strengthen the case. The first is components with different scaling needs, such as a CPU-heavy pipeline next to a lightly used admin API, because only then does independent scaling save money. The second is an existing platform capability (CI/CD templates, observability, service discovery) so that each new service does not pay its fixed costs from scratch.
When to avoid them
Avoid microservices with a small team, uniform load, or domain boundaries you cannot yet describe with confidence. Premature decomposition fixes your current guesses into network contracts, separate datastores, and deployment pipelines. When a boundary turns out wrong, merging two services means reconciling schemas, migrating data, and retiring APIs that other services call. That costs far more than moving a module inside one codebase.
Waiting has its own price. Decomposing a large monolith late means extracting services while the system is in production, often with tangled shared tables and implicit coupling that only surfaces under load.
The worst outcome comes from a mismatch with team structure. If service boundaries cut across team ownership, every feature still requires coordinated changes and lockstep deploys. The result is a distributed monolith. You pay for network calls, duplicated infrastructure, and cross-service debugging, but you get none of the independent delivery that justified the split.
Alternatives and cost-control tactics, and how to measure your own system
Avoiding that outcome doesn't require going all the way to microservices. A modular monolith keeps one deployable unit but enforces boundaries in code: each module has its own package and owns its own tables, and other modules can't call its internals. Selective extraction moves out only the one or two components that need it. Both options have costs. You can't deploy modules independently, and fault isolation is weak, so a memory leak or a bad deploy in one module can take down the whole process. Put that in your model as expected incident cost: incidents per year × hours to recover × cost per hour of downtime.
| Option | Where it saves | Where it costs |
|---|---|---|
| Modular monolith | One pipeline, one runtime, no network hops | Shared blast radius, coordinated releases |
| Selective extraction | Scales or isolates only the hot paths | Platform overhead for a few services |
| Microservices | Independent deploys and scaling | Fixed costs for every service, more coordination |
If you run microservices, merge low-traffic services that usually change together, because each one still pays the full fixed cost. Use shared platform templates so teams don't rebuild pipelines, dashboards and alerts for every new service. Sample traces: keep errors and slow requests, and sample the rest. Keep chatty call paths within one availability zone, because cross-zone traffic is often billed.
If you run a monolith, modularize internally and run builds and tests in parallel so CI time stays flat as the codebase grows. Extract a component only when it has distinct scaling or ownership needs.
To measure your own system, tag every resource with consistent keys, for example service=checkout, team=payments, env=prod, cost-center=commerce. Enforce the tags when resources are created so untagged spend shows up in billing. Track CI pipeline minutes and observability ingestion per service. For a couple of sprints, sample how many engineer hours go to cross-team coordination and platform upkeep.
# Same four keys used for cloud resource tags, applied as Kubernetes labels.
# Keep the key names identical everywhere so billing, CI and observability
# data can all be grouped by service, team and cost center.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
namespace: commerce
labels:
service: checkout
team: payments
env: prod
cost-center: commerce
spec:
replicas: 3
selector:
matchLabels:
service: checkout
template:
metadata:
# Pods carry the labels too. Per-pod CPU, memory and log volume
# is attributed from pod labels, not from the Deployment's labels.
labels:
service: checkout
team: payments
env: prod
cost-center: commerce
spec:
containers:
- name: checkout
image: registry.example.com/commerce/checkout:1.0.0
resources:
# Requests are what you pay to reserve. Set them so cost
# per service reflects reserved capacity, not just usage.
requests:
cpu: 250m
memory: 256Mi
A Kubernetes Deployment that uses the same service, team, env and cost-center keys as the cloud resource tags, applied to both the Deployment and its pods so spend can be attributed per service.
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 monolith vs microservices cost, 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
.envby 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.