The Hidden Costs of Microservices: When Monoliths Save Money

A Series B startup. 23 microservices. 10,000 daily active users.

Their AWS bill: $47,000/month.

After consolidating to 4 services: $18,000/month.

That's $348,000 saved per year. Not from clever optimization. Not from better caching. Just from admitting they didn't need microservices.

The Lies We Tell Ourselves

Lie #1: "We Need Microservices to Scale"

Netflix handles 100,000 requests per second. You handle 100.

You're cosplaying as Netflix with 0.1% of their traffic. And paying enterprise prices for startup problems.

Here's the truth: A single m5.xlarge EC2 instance can handle 10,000 requests per second for typical CRUD operations. Cost: $140/month.

Your 23 microservices on t3.medium instances? $1,150/month. For 100 requests per second.

Lie #2: "Microservices Make Development Faster"

20 repositories. 20 CI/CD pipelines. 20 sets of dependencies.

One feature touches 5 services? That's 5 pull requests, 5 code reviews, 5 deployments, 5 places where something can break.

Your "2-day feature" just became a 2-week distributed systems debugging session.

Lie #3: "It's Industry Best Practice"

Best practice for whom? Amazon with 10,000 engineers? Or your 15-person startup?

Amazon created microservices because hundreds of teams were stepping on each other. You created microservices because a Medium article told you to.

The Hidden Invoice

Pull up your AWS Cost Explorer. I'll wait.

Now let's find the costs you're not seeing:

Data Transfer: The Silent Killer

Every API call between your services costs money. AWS charges $0.01/GB for cross-AZ transfer.

Let's do kindergarten math:

  • User request → API Gateway → Service A → Service B → Service C → Database

  • Each hop: ~10KB request + 50KB response = 60KB

  • 1 million requests/day × 60KB × 5 hops = 300GB/day

  • Monthly damage: $900 just playing hot potato with your own data

Your monolith? Zero internal transfer costs. Zero.

The Load Balancer Tax

Each microservice needs an Application Load Balancer. AWS charges $0.025/hour + data processing fees.

20 services × $18/month = $360/month

Plus $0.008/GB processed. With 1TB/month per service: another $160.

Total: $520/month for the privilege of having multiple services.

One monolith needs one load balancer. $18/month. Done.

The Observability Black Hole

DataDog charges per host. CloudWatch charges per log group. New Relic charges per service.

Real numbers from a real startup:

  • DataDog: 20 hosts × $100/month = $2,000

  • CloudWatch Logs: 20 log groups × 50GB each = $500

  • CloudWatch Metrics: 200 custom metrics × $0.30 = $60

  • X-Ray tracing: 10 million traces × $0.000005 = $50

Total: $2,610/month to watch your 20 services do nothing special.

Same app as a monolith: $300/month.

Database Proliferation Disease

The microservices textbook says each service needs its own database.

20 services × RDS t3.small ($35/month) = $700/month

Plus backup storage, plus snapshots, plus read replicas for the important ones.

Real total: $1,400/month.

One RDS t3.xlarge handling everything: $280/month. With better performance.

The NAT Gateway Scam

Your private microservices need internet access. NAT Gateway: $45/month + $0.045/GB.

20 services downloading packages, pulling images, sending webhooks = 2TB/month.

NAT Gateway bill: $135/month.

Monolith in public subnet with security groups: $0/month.

When Microservices Actually Make Sense

Stop defending your architecture. Answer these honestly:

Do you have teams that CANNOT coordinate releases? Not "prefer not to." Not "it's annoying." CANNOT. If your teams can sit in one Zoom call, you don't need microservices.

Do different parts scale at 100x different rates? Not 2x. Not 10x. ONE HUNDRED TIMES. Your user service gets 1,000 req/sec while your PDF generator gets 10? That's 100x. You win. Extract it.

Do you NEED different programming languages? Not want. NEED. Machine learning in Python while everything else is in Java? Fine. Extract just that.

No to all three? You're burning $30,000/month for architectural purity.

The Smart Monolith Pattern

One codebase. Multiple modules. Clear boundaries. No network calls.

app/
├── billing/        # Domain logic
├── users/          # Domain logic  
├── notifications/  # Domain logic
├── api/           # HTTP layer
└── shared/        # Common utilities

Each module has its own:

  • Domain models

  • Business logic

  • Database schema (same database, different schemas)

  • Tests

What it doesn't have:

  • Separate repository

  • Separate CI/CD pipeline

  • Separate monitoring

  • Separate deployment

  • Network calls to other modules

When you hit 10,000 requests per second (you won't), the boundaries are ready. Extract then. Not now.

The Extraction Threshold

Only extract a service when:

It's costing you customers The monolith literally cannot handle the load. Not "might not" in the future. Cannot. Today. Customers are leaving.

It's costing you engineers Teams are blocked weekly because of deployment conflicts. Not occasionally annoyed. Blocked. Weekly. Productivity is measurably dying.

It's costing you compliance PCI DSS requires payment processing isolation. HIPAA requires healthcare data separation. The auditor says "separate system" and means it.

Everything else is resume-driven development.

The Migration Math

Your current setup:

  • 20 microservices

  • 5-person engineering team

  • 100 requests/second peak traffic

  • $15,000/month infrastructure

After consolidation to 3 services:

  • API monolith (95% of your logic)

  • Background jobs (the stuff that actually needs different scaling)

  • Notifications (the thing that spikes 10x during campaigns)

New costs:

  • Infrastructure: $4,000/month

  • Saved: $11,000/month

  • Annual savings: $132,000

That's two engineers' salaries. Or seed funding runway extension. Or actual features instead of fixing distributed system bugs.

Your Action Items Right Now

  1. Run this query in CloudWatch Insights: Find out how many internal API calls you're making. Multiply by $0.01/GB. Cry.

  2. Count your Load Balancers: Go to EC2 → Load Balancers. Count them. Multiply by $18. That's your ALB tax.

  3. Check your DataDog bill: Settings → Usage → Hosts. How many are just the same app split up?

  4. List services owned by the same team: If the same people own multiple services, why are they separate?

The truth nobody wants to hear: Your startup doesn't need microservices. You need a product that works, customers who pay, and money in the bank.

Stop playing distributed systems architect. Start shipping.

Your bank account will thank you.

Done For You Scripts + Step-By-Step Guides

I’ve created done-for-you scripts and detailed how-tos that will instantly reduce your cloud spend by migrating from unedded microservices to smart monolith, Here is what you will get below:

Zero-AWS Access Calculator - Python script to calculate savings by simply inputting your EC2, RDS, ALB, and NAT Gateway counts
30% Efficiency Gain - Monoliths use 30% fewer resources due to eliminated network overhead and better resource utilization
Real Cost Breakdown - See exactly how much you're spending on each microservice component vs consolidated infrastructure
Instant ROI Analysis - Calculator shows monthly/annual savings and flags if you're burning an engineer's salary on complexity
Strangler Pattern Migration - Safe consolidation using NGINX shadow traffic mirroring - no big-bang rewrites required
Zero-Downtime Database Merge - Consolidate multiple small databases into one properly-sized instance using schema separation
Smart Monolith Architecture - Domain-driven folder structure that's extraction-ready
Docker Compose Setup - Local development environment that perfectly mirrors production
Traffic Migration Strategy - Gradual weighted routing from microservices to monolith with instant rollback capability
Environment Variable Switching - Toggle between monolith and microservice databases with a single config change

The Monolith Cost Calculator Script

This script requires zero AWS access - just edit the configuration at the top with your actual resource counts and see how much you're wasting on microservices complexity.

#!/usr/bin/env python3
"""
Monolith Cost Calculator - See what you'd save by consolidating
No AWS access needed - just count your resources and edit below
"""

# ============================================================
# EDIT THIS SECTION WITH YOUR ACTUAL INFRASTRUCTURE
# ============================================================

my_services = {
    'ec2': {
        't3.micro': 0,      # How many t3.micro instances?
        't3.small': 5,      # How many t3.small instances?
        't3.medium': 15,    # How many t3.medium instances?
        't3.large': 0,      # How many t3.large instances?
        't3.xlarge': 0,     # How many t3.xlarge instances?
        't3.2xlarge': 0,    # How many t3.2xlarge instances?
        'm5.large': 3,      # How many m5.large instances?
        'm5.xlarge': 0,     # How many m5.xlarge instances?
        'm5.2xlarge': 0,    # How many m5.2xlarge instances?
    },
    'rds': {
        'db.t3.micro': 0,    # How many db.t3.micro databases?
        'db.t3.small': 8,    # How many db.t3.small databases?
        'db.t3.medium': 3,   # How many db.t3.medium databases?
        'db.t3.large': 0,    # How many db.t3.large databases?
        'db.t4g.medium': 0,  # How many db.t4g.medium databases?
        'db.t4g.large': 0,   # How many db.t4g.large databases?
        'db.m5.large': 0,    # How many db.m5.large databases?
        'db.m5.xlarge': 0,   # How many db.m5.xlarge databases?
    },
    'alb_count': 20,        # How many Application Load Balancers?
    'nat_gateway_count': 3,  # How many NAT Gateways?
}

# ============================================================
# DON'T EDIT BELOW THIS LINE
# ============================================================

def calculate_current_costs(services):
    """Calculate current microservices infrastructure costs"""
    
    # AWS pricing (us-east-1, on-demand, monthly)
    ec2_monthly = {
        't3.micro': 7.49,
        't3.small': 15.18,
        't3.medium': 30.37,
        't3.large': 60.74,
        't3.xlarge': 121.47,
        't3.2xlarge': 242.94,
        'm5.large': 69.12,
        'm5.xlarge': 138.24,
        'm5.2xlarge': 276.48,
    }
    
    rds_monthly = {
        'db.t3.micro': 12.41,
        'db.t3.small': 24.82,
        'db.t3.medium': 49.64,
        'db.t3.large': 99.28,
        'db.t4g.medium': 45.26,
        'db.t4g.large': 90.52,
        'db.m5.large': 128.52,
        'db.m5.xlarge': 257.04,
    }
    
    total = 0
    breakdown = {}
    
    # EC2 costs
    for instance_type, count in services['ec2'].items():
        if count > 0:
            cost = ec2_monthly.get(instance_type, 50) * count
            total += cost
            breakdown[f'EC2 {instance_type}'] = {'count': count, 'monthly': cost}
    
    # RDS costs
    for instance_type, count in services['rds'].items():
        if count > 0:
            cost = rds_monthly.get(instance_type, 100) * count
            total += cost
            breakdown[f'RDS {instance_type}'] = {'count': count, 'monthly': cost}
    
    # Load balancers
    alb_cost = services.get('alb_count', 0) * 18
    if alb_cost > 0:
        total += alb_cost
        breakdown['Load Balancers'] = {'count': services.get('alb_count', 0), 'monthly': alb_cost}
    
    # NAT Gateways
    nat_cost = services.get('nat_gateway_count', 0) * 32
    if nat_cost > 0:
        total += nat_cost
        breakdown['NAT Gateways'] = {'count': services.get('nat_gateway_count', 0), 'monthly': nat_cost}
    
    # Data transfer estimate (inter-service communication)
    service_count = sum(services['ec2'].values())
    if service_count > 1:
        # Rough estimate: each service talks to 3 others, 100GB/month each
        transfer_cost = min(service_count * 3, service_count * (service_count-1) / 2) * 1.0  # $1 per connection
        total += transfer_cost
        breakdown['Data Transfer (est)'] = {'count': f'{service_count} services', 'monthly': transfer_cost}
    
    return total, breakdown

def calculate_monolith_costs(services):
    """Calculate equivalent monolith infrastructure costs"""
    
    # Calculate total compute needed
    total_vcpus = 0
    vcpu_map = {
        't3.micro': 2, 't3.small': 2, 't3.medium': 2, 
        't3.large': 2, 't3.xlarge': 4, 't3.2xlarge': 8,
        'm5.large': 2, 'm5.xlarge': 4, 'm5.2xlarge': 8
    }
    
    for instance_type, count in services['ec2'].items():
        total_vcpus += vcpu_map.get(instance_type, 2) * count
    
    # Monolith is 30% more efficient (no network overhead, better resource usage)
    needed_vcpus = int(total_vcpus * 0.7)
    
    # Size the monolith
    monolith = {}
    if needed_vcpus <= 4:
        monolith['ec2'] = {'t3.xlarge': 1}
    elif needed_vcpus <= 8:
        monolith['ec2'] = {'t3.2xlarge': 1}
    elif needed_vcpus <= 16:
        monolith['ec2'] = {'m5.2xlarge': 2}
    else:
        instance_count = max(3, needed_vcpus // 8)  # At least 3 for HA
        monolith['ec2'] = {'m5.2xlarge': instance_count}
    
    # Database consolidation
    total_db_count = sum(services['rds'].values())
    if total_db_count == 0:
        monolith['rds'] = {}
    elif total_db_count <= 3:
        monolith['rds'] = {'db.t4g.large': 1}
    elif total_db_count <= 10:
        monolith['rds'] = {'db.m5.xlarge': 1}
    else:
        monolith['rds'] = {'db.m5.xlarge': 2}
    
    # Monolith needs minimal infrastructure
    monolith['alb_count'] = 1  # Just one ALB
    monolith['nat_gateway_count'] = 0  # Public subnet with security groups
    
    return calculate_current_costs(monolith)

def generate_report(services):
    """Generate the cost comparison report"""
    
    current_cost, current_breakdown = calculate_current_costs(services)
    monolith_cost, monolith_breakdown = calculate_monolith_costs(services)
    monthly_savings = current_cost - monolith_cost
    
    print("\n" + "="*60)
    print("💰 MICROSERVICES VS MONOLITH COST ANALYSIS")
    print("="*60)
    
    print("\n📊 CURRENT MICROSERVICES SETUP:")
    for item, details in current_breakdown.items():
        print(f"  {item}: {details['count']} × ${details['monthly']/details['count'] if details['count'] else 0:.2f} = ${details['monthly']:.2f}/mo")
    print(f"\n  TOTAL: ${current_cost:,.2f}/month (${current_cost*12:,.2f}/year)")
    
    print("\n🎯 EQUIVALENT MONOLITH SETUP:")
    for item, details in monolith_breakdown.items():
        if details['monthly'] > 0:
            print(f"  {item}: {details['count']} × ${details['monthly']/details['count'] if details['count'] else 0:.2f} = ${details['monthly']:.2f}/mo")
    print(f"\n  TOTAL: ${monolith_cost:,.2f}/month (${monolith_cost*12:,.2f}/year)")
    
    print("\n" + "="*60)
    print(f"💸 MONTHLY SAVINGS: ${monthly_savings:,.2f}")
    print(f"💰 ANNUAL SAVINGS: ${monthly_savings * 12:,.2f}")
    print(f"📉 COST REDUCTION: {(monthly_savings/current_cost)*100:.1f}%")
    print("="*60)
    
    if monthly_savings > 10000:
        print("\n🚨 HOLY SH*T: You're burning a senior engineer's salary on complexity!")
    elif monthly_savings > 5000:
        print("\n⚠️  WARNING: That's a junior engineer's salary in savings!")
    elif monthly_savings > 1000:
        print("\n💡 That's real money. Time to consolidate.")
    else:
        print("\n✅ Your architecture is reasonably cost-efficient.")
    
    return monthly_savings

if __name__ == "__main__":
    savings = generate_report(my_services)
    
    if savings > 1000:
        print("\n📝 NEXT STEPS:")
        print("1. Start with services owned by the same team")
        print("2. Consolidate read-heavy services first")
        print("3. Keep services with different scaling patterns separate")
        print("4. Use the strangler pattern - no big bang 
rewrites")

Exmple output from calculator to tell you savings:


$ python monolith_calculator.py

============================================================
💰 MICROSERVICES VS MONOLITH COST ANALYSIS
============================================================

📊 CURRENT MICROSERVICES SETUP:
  EC2 t3.small: 5 × $15.18 = $75.90/mo
  EC2 t3.medium: 15 × $30.37 = $455.55/mo
  EC2 m5.large: 3 × $69.12 = $207.36/mo
  RDS db.t3.small: 8 × $24.82 = $198.56/mo
  RDS db.t3.medium: 3 × $49.64 = $148.92/mo
  Load Balancers: 20 × $18.00 = $360.00/mo
  NAT Gateways: 3 × $32.00 = $96.00/mo
  Data Transfer (est): 23 services = $23.00/mo

  TOTAL: $1,565.29/month ($18,783.48/year)

🎯 EQUIVALENT MONOLITH SETUP:
  EC2 m5.2xlarge: 3 × $276.48 = $829.44/mo
  RDS db.m5.xlarge: 1 × $257.04 = $257.04/mo
  Load Balancers: 1 × $18.00 = $18.00/mo

  TOTAL: $1,104.48/month ($13,253.76/year)

============================================================
💸 MONTHLY SAVINGS: $460.81
💰 ANNUAL SAVINGS: $5,529.72
📉 COST REDUCTION: 29.4%
============================================================

💡 That's real money. Time to consolidate.

📝 NEXT STEPS:
1. Start with services owned by the same team
2. Consolidate read-heavy services first
3. Keep services with different scaling patterns separate
4. Use the strangler pattern - no big bang rewrites

The Strangler Consolidation Pattern

Instead of a risky big-bang migration, gradually strangle your microservices by consolidating them behind their existing API contracts.

Phase 1: Shadow Mode Deploy your monolith alongside existing services. Use NGINX to duplicate traffic with the mirror feature

# nginx.conf snippet for shadow testing
upstream microservices {
    server user-service.internal:8080;
    server order-service.internal:8080;
}

upstream monolith {
    server monolith.internal:8080;
}

location /api/ {
    proxy_pass http://microservices;
    
    # Shadow traffic to monolith (doesn't affect response)
    mirror /shadow;
}

location /shadow {
    internal;
    proxy_pass http://monolith$request_uri;
}

Key Points:

  • Never break existing connections

  • Monitor error rates at each step

  • Keep rollback ready (just update weights back)

Database Consolidation Without Downtime

Merge your 20 tiny databases into one properly-sized instance without losing data or causing downtime.

Step 1: Create Consolidated Instance

bash

# Create new RDS instance with multiple schemas
aws rds create-db-instance \
    --db-instance-identifier monolith-db \
    --db-instance-class db.m5.xlarge \
    --engine postgres \
    --master-username admin \
    --master-user-password $PASSWORD \
    --allocated-storage 500

Use this environment variable to easily switch between monolith and microservice while you migrate

# Zero-downtime connection switch using environment variables
# Deploy this to your services gradually
import os

def get_db_connection():
    if os.getenv('USE_MONOLITH_DB', 'false').lower() == 'true':
        return {
            'host': 'monolith-db.internal',
            'port': 5432,
            'database': 'monolith',
            'schema': os.getenv('SERVICE_NAME')  # Each keeps its schema
        }
    else:
        # Original microservice DB
        return {
            'host': f"{os.getenv('SERVICE_NAME')}-db.internal",
            'port': 5432,
            'database': os.getenv('SERVICE_NAME')
        }

The Smart Monolith Architecture

Structure your monolith to be ready for extraction (but you probably won't need to).

Project Structure:

monolith/
├── docker-compose.yml       # Local dev matching prod
├── Dockerfile
├── src/
│   ├── api/                # HTTP layer
│   │   ├── users.py
│   │   ├── orders.py
│   │   └── router.py
│   ├── domains/            # Business logic
│   │   ├── users/
│   │   │   ├── service.py
│   │   │   ├── repository.py
│   │   │   └── models.py
│   │   ├── orders/
│   │   └── shared/
│   └── infrastructure/     # Cross-cutting concerns
│       ├── database.py
│       └── messaging.py

Docker Compose for Local = Production:

# docker-compose.yml
version: '3.8'
services:
  monolith:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis
  
  postgres:
    image: postgres:14
    environment:
      POSTGRES_DB: monolith
    volumes:
      - ./init-schemas.sql:/docker-entrypoint-initdb.d/init.sql
  
  redis:
    image: redis:7

With this info you should be good to go

Keep Reading

No posts found