← Back to list

EKS Cost Saving Tips — From Cluster Setup to Microservice Connections, Storage, and Beyond

Running EKS in production is powerful — but expensive if you don’t pay attention. Here are practical cost saving tips from real production…

Sai Pavan Puligadda · 2026-06-22 04:01 · 0 claps · 9.7 min read
#aws-eks #cost-optimization #kubernetes #cost-saving-tips #cloud-computing
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval PFI · Personal Finance ☁️ · DevOps & Cloud 🏃 · Running & Endurance

EKS Cost Saving Tips — From Cluster Setup to Microservice Connections, Storage, and Beyond

Running EKS in production is powerful — but expensive if you don’t pay attention. Here are practical cost saving tips from real production experience, covering cluster setup, AWS service connections, storage, compute, secrets management, and pod resource allocation.

1. EKS Cluster Setup — hidden costs people miss

Control plane cost

Every EKS cluster costs $0.10/hour (~$72/month) regardless of whether you run any workloads. If you have separate dev, staging, and prod clusters, that’s $216/month just for control planes.

💡 Tip — Merge dev/staging into one cluster with namespaces

Use namespace isolation for dev and staging instead of separate clusters. Save $72–144/month on control plane costs. Use RBAC to enforce team separation. Only prod gets its own dedicated cluster.

# Instead of 3 clusters (dev, staging, prod)
# Use 2 clusters: non-prod + prod

# non-prod cluster — namespaces for isolation
kubectl create namespace dev
kubectl create namespace staging

# Apply resource quotas per namespace to prevent runaway costs
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    count/pods: "20"

EKS add-ons — only install what you need

Every add-on runs pods that consume CPU and memory on your nodes. Common unnecessary add-ons teams leave running:

Audit your add-ons regularly: kubectl get pods -n kube-system — if you see CoreDNS running with 2 replicas on a small cluster, that’s wasted resources. Scale CoreDNS down to 1 replica on non-prod clusters.

# Check running add-on pods and their resource consumption
kubectl top pods -n kube-system --sort-by=memory

# Scale CoreDNS to 1 replica on dev/staging (not prod!)
kubectl scale deployment coredns --replicas=1 -n kube-system

# Remove unused add-ons via Helm
helm list -n kube-system   # audit what's installed
helm uninstall  -n kube-system

2. Node Groups — ARM vs AMD, Spot vs On-Demand

Use ARM (Graviton) instances where possible Save 20–40%

AWS Graviton3 (ARM64) instances are 20–40% cheaper than equivalent AMD/Intel instances and often faster for containerised workloads. If your Docker images support multi-arch (AMD64 + ARM64) — which they should — move non-critical workloads to Graviton.

# Karpenter NodePool — prefer ARM Graviton instances
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: general
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["arm64", "amd64"]   # allow both, Karpenter picks cheapest
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: node.kubernetes.io/instance-type
          operator: In
          values:
            - m7g.medium      # Graviton3 — cheapest general purpose
            - m7g.large
            - m7g.xlarge
            - m6g.medium      # Graviton2 — fallback
            - m6g.large
            - m5.large        # AMD fallback if no ARM available

Use Spot instances for non-critical workloads Save 60–90%

# Separate node pools for spot vs on-demand
# Spot — for dev, staging, batch jobs, stateless services
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: spot-pool
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
      taints:
        - key: spot
          value: "true"
          effect: NoSchedule

# On-demand — for prod stateful services only
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: ondemand-prod
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]

Real impact: At Syntizen we ran dev/staging on Spot instances via Karpenter. Combined with right-sizing, this contributed significantly to the 36% cost reduction ($22K → $14K). Spot interruptions in dev/staging are acceptable — design your CI/CD to handle pod restarts gracefully.

3. Pod Resource Requests and Limits — the biggest hidden cost

Incorrect resource requests are the #1 cause of wasted AWS spend on EKS. Over-requesting CPU/memory means you’re paying for resources your pods never use.

The golden rule — requests vs limits

# ❌ BAD — over-provisioned, wastes money
resources:
  requests:
    cpu: "2000m"      # pod uses avg 200m but requests 2000m
    memory: "4Gi"     # pod uses avg 512Mi but requests 4Gi
  limits:
    cpu: "4000m"
    memory: "8Gi"

# ✅ GOOD — right-sized based on actual usage
resources:
  requests:
    cpu: "200m"       # set at p50 actual usage
    memory: "512Mi"   # set at p75 actual usage
  limits:
    cpu: "1000m"      # 5x request — allows burst
    memory: "1Gi"     # 2x request — prevents OOM cascade

How to find actual usage: Run your workload for 1 week, then check Datadog or run kubectl top pods -n production — sort-by=cpu. Set requests at p50 usage, limits at 2–5x requests. Never set requests = limits (this kills bin-packing efficiency).

Memory-optimised vs compute-optimised node selection

# For memory-heavy services (e.g. Java, Elasticsearch, Redis)
# Use r-series Graviton instances
nodeSelector:
  node.kubernetes.io/instance-type: r7g.large  # 16GB RAM, 2 vCPU

# For CPU-heavy services (e.g. video processing, ML inference)
# Use c-series instances
nodeSelector:
  node.kubernetes.io/instance-type: c7g.xlarge  # 8 vCPU, 16GB RAM

# Label nodes by workload type via Karpenter
requirements:
  - key: karpenter.k8s.aws/instance-family
    operator: In
    values: ["r7g", "r6g"]   # memory optimised Graviton

4. Connecting to MongoDB Atlas Data transfer cost

MongoDB Atlas runs outside AWS. Every byte of data between your EKS pods and Atlas crosses the public internet — and AWS charges for outbound data transfer.

💡 Tip 1 — Use Atlas AWS Private Endpoint (VPC Peering)

Enable AWS PrivateLink between your VPC and Atlas. Traffic stays within AWS backbone — no public internet, no NAT Gateway charges, lower latency. Atlas charges a small fee for private endpoints but it saves more on data transfer at scale.

💡 Tip 2 — Deploy Atlas cluster in the SAME AWS region

Cross-region data transfer from us-east-1 EKS to eu-west-1 Atlas costs ~$0.02/GB. If your Atlas cluster is in a different region, evaluate moving it to match your EKS region — especially if you have high read/write volume.

💡 Tip 3 — Connection pooling — don’t open a new connection per request

Each MongoDB connection has overhead. Use a connection pool with a max size appropriate for your pod count. Over-connecting is expensive and causes Atlas to throttle.

# MongoDB connection pool in Node.js — cost-efficient config
const client = new MongoClient(uri, {
  maxPoolSize: 10,        // max connections per pod
  minPoolSize: 2,         // keep 2 connections warm
  maxIdleTimeMS: 30000,   // close idle connections after 30s
  serverSelectionTimeoutMS: 5000,
});

5. Redis on AWS (ElastiCache) — same vs cross-region

💡 Always deploy ElastiCache in the SAME region and SAME AZ as your EKS nodes

Cross-AZ data transfer costs $0.01/GB each way. For a high-traffic Redis cache handling millions of requests, this adds up fast. Deploy your ElastiCache subnet in the same AZ as your primary node group.

# Check which AZ your nodes are in
kubectl get nodes -o wide | awk '{print $7}'

# Deploy ElastiCache in matching AZ via Terraform
resource "aws_elasticache_subnet_group" "redis" {
  name       = "eks-redis-subnet"
  subnet_ids = [aws_subnet.private_us_east_1a.id]  # match EKS node AZ
}
# Use VPC endpoint for ElastiCache - no NAT Gateway needed
# ElastiCache in same VPC = no data transfer charges at all

Same VPC = zero data transfer cost. ElastiCache in the same VPC as EKS incurs no AWS data transfer charges. Cross-region Redis replication is expensive — only do it if you genuinely need multi-region failover.

6. S3 — same vs cross-region access

💡 Tip 1 — Use VPC Gateway Endpoint for S3 Free

By default, S3 traffic from your EKS pods goes through the NAT Gateway — which costs $0.045/GB processed. A VPC Gateway Endpoint for S3 routes traffic within AWS backbone at zero cost. This is free to set up and takes 5 minutes.

# Add S3 VPC Gateway Endpoint via Terraform — FREE
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = aws_route_table.private[*].id
  # Zero cost — saves NAT Gateway processing charges
}

# After this, S3 traffic from EKS no longer goes through NAT Gateway

💡 Tip 2 — Cross-region S3 access costs money

Accessing an S3 bucket in us-west-2 from EKS in us-east-1 costs $0.02/GB for data transfer. Keep S3 buckets in the same region as your EKS cluster. For multi-region access, use S3 Multi-Region Access Points or replicate buckets.

💡 Tip 3 — Use S3 Intelligent-Tiering for infrequently accessed data

If your pods write logs or backups to S3, enable Intelligent-Tiering. Objects not accessed for 30 days automatically move to cheaper storage tiers. Save 40–68% on storage costs for cold data.

7. SQS / SNS — cost gotchas

💡 Use long polling on SQS — not short polling

Short polling (default) makes an API call every few seconds even when the queue is empty. Each API call costs money. Long polling waits up to 20 seconds for a message — dramatically reduces empty receive calls and cost.

# SQS consumer in your pod — use long polling
import boto3
sqs = boto3.client('sqs')

# ❌ Short polling — expensive, many empty calls
response = sqs.receive_message(QueueUrl=queue_url)

# ✅ Long polling — cost efficient
response = sqs.receive_message(
    QueueUrl=queue_url,
    WaitTimeSeconds=20,      # wait up to 20s for messages
    MaxNumberOfMessages=10,  # batch — process 10 at once
)

💡 Use VPC Endpoint for SQS/SNS

Same principle as S3 — add Interface VPC Endpoints for SQS and SNS so traffic doesn’t route through NAT Gateway. Each endpoint costs ~$7/month but saves NAT Gateway processing at scale.

8. Lambda invocations from EKS pods

💡 Use async invocation where possible

Synchronous Lambda invocation holds your pod thread open waiting for response. For fire-and-forget use cases (sending emails, triggering jobs), use async invocation — your pod is free immediately.

# Sync invocation — pod waits for Lambda response
response = lambda_client.invoke(
    FunctionName='my-function',
    InvocationType='RequestResponse',  # synchronous — pod waits
)

# Async invocation — pod fires and continues immediately
response = lambda_client.invoke(
    FunctionName='my-function',
    InvocationType='Event',            # async — no waiting
)

# Even better — use SQS as trigger instead of direct invocation
# Pod → SQS → Lambda (fully decoupled, no direct connection cost)

💡 Use VPC Endpoint for Lambda — avoid NAT Gateway charges

If your EKS pods invoke Lambda frequently, add an Interface VPC Endpoint for Lambda. Traffic stays within AWS network — no NAT Gateway processing fees.

9. EBS vs EFS as PVC — choose wisely

EBS (gp3) — cheaper, single AZ

# EBS gp3 StorageClass — cheaper than gp2, better performance
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-gp3
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"        # baseline IOPS included free with gp3
  throughput: "125"   # MB/s included free with gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer  # provision in pod's AZ

gp3 is always cheaper than gp2 for the same size. gp3 starts at $0.08/GB/month vs gp2 at $0.10/GB/month — and gp3 includes 3000 IOPS and 125 MB/s free. Migrate all existing gp2 PVCs to gp3 immediately.

EFS — expensive, use only when you need shared storage

# EFS costs $0.30/GB/month vs EBS gp3 at $0.08/GB/month
# EFS is 3.75x MORE expensive than EBS
# Only use EFS when you need:
# - ReadWriteMany (multiple pods writing to same volume)
# - Cross-AZ access to same volume
# - Shared config files across pods

# If only ONE pod needs the volume — always use EBS gp3
# If MULTIPLE pods need same volume — use EFS (no choice)

10. AWS Secrets Manager (ASM) — API call costs

Every time your pod calls secretsmanager:GetSecretValue it costs $0.05 per 10,000 API calls. Sounds cheap — but at scale with hundreds of pods starting and restarting frequently, it adds up.

💡 Use External Secrets Operator — don’t call ASM from application code

ESO fetches the secret once on a schedule (e.g. every 1hr) and stores it as a Kubernetes Secret. Your pods read the K8s Secret — zero ASM API calls at runtime. Compared to each pod calling ASM on every startup, this reduces API calls by 99%+.

# ❌ EXPENSIVE — every pod startup calls ASM
import boto3
client = boto3.client('secretsmanager')
secret = client.get_secret_value(SecretId='prod/db/password')
# Called on every pod start, every restart, every scale-up event

# ✅ FREE at runtime — ESO syncs once, pod reads K8s Secret
# ExternalSecret refreshInterval: 1h → 1 ASM call per hour
# 100 pods restarting 10x/day = 0 extra ASM calls
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-credentials    # K8s Secret managed by ESO
        key: password

💡 Cache secrets in application memory — don’t fetch on every request

If you must call ASM from code, cache the secret in memory with a 1-hour TTL. Never call ASM on every HTTP request — that’s thousands of unnecessary API calls per minute.

11. NAT Gateway — the silent budget killer

NAT Gateway costs $0.045/GB processed + $0.045/hour per AZ (~$32/month). Every pod calling the internet (DockerHub, external APIs, MongoDB Atlas public endpoint) goes through NAT Gateway.

Add VPC Gateway Endpoints for S3 and DynamoDB — free, removes their traffic from NAT

Add VPC Interface Endpoints for ECR, SQS, SNS, Lambda, Secrets Manager — each ~$7/month but saves NAT processing at volume

Use ECR for all images — pulling from DockerHub goes through NAT. ECR in same region uses VPC endpoint = zero NAT cost

One NAT Gateway per region for non-prod — not one per AZ. Cross-AZ NAT traffic costs $0.01/GB but saves $32/month per removed NAT Gateway

# VPC endpoints to add — priority order by cost saving
# 1. S3 Gateway Endpoint — FREE, highest impact
# 2. ECR endpoints — saves DockerHub NAT traffic
# 3. Secrets Manager endpoint — saves ESO sync traffic
# 4. SQS/SNS endpoints — saves message queue traffic

resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.us-east-1.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

Quick savings summary

# Priority order by potential saving:
1. Right-size pod requests/limits        → 20-40% node cost reduction
2. Spot instances for non-prod           → 60-90% node cost reduction
3. ARM Graviton instances                → 20-40% instance cost reduction
4. S3 VPC Gateway Endpoint              → Eliminate S3 NAT charges (FREE)
5. ESO for secrets (not direct ASM)     → 99% reduction in ASM API calls
6. Merge dev/staging clusters           → Save $72/month per removed cluster
7. SQS long polling                     → Reduce empty receive API calls
8. gp3 instead of gp2 EBS              → 20% cheaper storage + free IOPS
9. ElastiCache same AZ as nodes        → Eliminate cross-AZ charges
10. ECR VPC endpoints                   → Eliminate DockerHub NAT traffic

Closing thoughts

AWS costs on EKS are death by a thousand cuts — no single item is catastrophic, but together they compound. The highest-impact changes are almost always pod right-sizing and Spot instances. After those, VPC endpoints and storage class optimisation deliver the next wave of savings with minimal engineering effort.

The 36% cost reduction I contributed to at Syntizen ($22K → $14K) came from applying exactly these principles systematically — right-sizing first, then eliminating idle resources, then optimising data transfer paths. Start with what’s measurable and work down the list.

AWS #EKS #CostOptimisation #Kubernetes #FinOps #Karpenter #DevOps #CloudNative #Terraform


메타데이터
post_id
61f67a50768a
slug
eks-cost-saving-tips-from-cluster-setup-to-microservice-connections-storage-and-beyond-61f67a50768a
url
https://medium.com/@saipavan-puligadda/eks-cost-saving-tips-from-cluster-setup-to-microservice-connections-storage-and-beyond-61f67a50768a
canonical_url
https://medium.com/@saipavan-puligadda/eks-cost-saving-tips-from-cluster-setup-to-microservice-connections-storage-and-beyond-61f67a50768a
author_url
https://medium.com/@saipavan-puligadda
status
ok
fetched_at
2026-07-08 02:40:31