<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alpacked</title>
    <description>The latest articles on DEV Community by Alpacked (@alpacked).</description>
    <link>https://dev.to/alpacked</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3961199%2F0f8d886c-3977-4a44-919c-373cb46ab26e.png</url>
      <title>DEV Community: Alpacked</title>
      <link>https://dev.to/alpacked</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alpacked"/>
    <language>en</language>
    <item>
      <title>The Self-Hosted LLM Breakeven Point Isn't 2M Tokens a Day. It's a Ratio.</title>
      <dc:creator>Alpacked</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:17:58 +0000</pubDate>
      <link>https://dev.to/alpacked/the-self-hosted-llm-breakeven-point-isnt-2m-tokens-a-day-its-a-ratio-5geh</link>
      <guid>https://dev.to/alpacked/the-self-hosted-llm-breakeven-point-isnt-2m-tokens-a-day-its-a-ratio-5geh</guid>
      <description>&lt;p&gt;Self-hosting an LLM is not automatically cheaper. This is the single most common mistake in these calculations: people price the GPU, compare it against last month's API bill, and walk away with a number that has almost nothing to do with what they'll actually spend.&lt;/p&gt;

&lt;p&gt;Savings show up under two conditions - when you've calculated the breakeven point for your specific workload, and when you've split traffic between a local model and an API instead of attempting a wholesale migration.&lt;/p&gt;

&lt;p&gt;Enterprise spending on model APIs more than doubled in six months - from $3.5 billion in late 2024 to $8.4 billion by mid-2025, according to Menlo Ventures. Inference in production, not model development, is what drove it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A production instance on a consumer GPU costs roughly &lt;strong&gt;$850/month&lt;/strong&gt;, and more than half of that is an engineer, not hardware.&lt;/li&gt;
&lt;li&gt;Breakeven isn't a token volume. It's &lt;strong&gt;$850 ÷ (your blended API price × 30)&lt;/strong&gt; - which lands anywhere from 2.3M to 94M tokens/day depending on which tier you're displacing.&lt;/li&gt;
&lt;li&gt;The "2M tokens/day" rule of thumb is correct for replacing frontier calls and badly wrong for anything cheaper.&lt;/li&gt;
&lt;li&gt;The architecture that works is hybrid: local for high-volume and regulated traffic, API for hard reasoning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why teams start doing this math
&lt;/h2&gt;

&lt;p&gt;Three pressures, usually arriving in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost scales linearly with volume.&lt;/strong&gt; There's no ceiling. The product grows, the bill grows with it. For an API that's both a feature (you don't pay for idle capacity) and a problem (you can't fix a budget).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data leaves your perimeter.&lt;/strong&gt; For HIPAA or PCI-DSS, an enterprise agreement is not a substitute for controlling your own infrastructure. Every prompt physically travels somewhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network round-trips.&lt;/strong&gt; 50–200ms per request, which you feel in any real-time UX.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What a local instance actually costs
&lt;/h2&gt;

&lt;p&gt;The figure $850/month shows up throughout this post as the cost of one production instance on a consumer GPU. Here's where it comes from - because in most write-ups this number is either pulled from thin air or collapsed into "the price of a graphics card."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmj65b2so52h5bgzhtmd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmj65b2so52h5bgzhtmd.png" alt=" " width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few notes on each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Capex.&lt;/strong&gt; The RTX 5090 carries an official MSRP of $1,999, but GDDR7 shortages kept street prices at $3,500–4,000+ through early 2026. Add a 1,200W+ PSU and serious cooling for its 575W TDP, plus ~$1,200 for CPU, RAM, and chassis. Check current pricing before you build this into a business case - the memory market moves in weeks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Electricity.&lt;/strong&gt; Substitute your own rate. The spread between jurisdictions is severalfold, not a rounding error. An RTX 4090 at 450W still costs $50+/month for the card alone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operations.&lt;/strong&gt; This is the largest line item and the one everyone underestimates. An MLOps engineer who updates models, debugs CUDA errors, and keeps monitoring honest costs more than all the hardware combined.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More than half of that total isn't silicon or electricity - it's a person. &lt;strong&gt;If nobody on the team has that skill set, self-hosting isn't cheaper.&lt;/strong&gt; It just moves the cost into a budget line that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this number matters&lt;/strong&gt;&lt;br&gt;
The important property of $850 is that it's fixed. It doesn't move until the server hits its throughput ceiling. Everything below is a comparison between a flat line and a linear one.&lt;/p&gt;
&lt;h2&gt;
  
  
  The breakeven point isn't a volume - it's a ratio
&lt;/h2&gt;

&lt;p&gt;The industry rule of thumb says self-hosting becomes competitive above 2 million tokens per day. The rule works, but it's incomplete: it says nothing about what you're replacing. That turns out to be the dominant variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - Blended price per tier&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At a 70% input / 30% output split:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjhyuv7rj6e45xu8f0ump.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjhyuv7rj6e45xu8f0ump.png" alt=" " width="799" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That 70/30 split is doing real work here, so adjust it. Chat and summarization workloads skew further toward input and land cheaper; agentic loops that generate long tool calls skew toward output and land considerably more expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 - Monthly bill by volume&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxljz71cggdaugs2jqwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxljz71cggdaugs2jqwy.png" alt=" " width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The formulas:&lt;br&gt;
&lt;code&gt;monthly API cost = volume in M/day × price per 1M × 30&lt;br&gt;
breakeven volume = infrastructure cost ÷ (price per 1M × 30)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 - Where "2M" actually comes from&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Plug $850 into the second formula and the answer splits three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Against the &lt;strong&gt;cheap tier - ~94M tokens/day&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Against &lt;strong&gt;mid tier - ~5.7M tokens/day&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Against &lt;strong&gt;frontier - ~2.3M tokens/day&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There it is. The 2M rule is the breakeven for displacing &lt;strong&gt;frontier calls&lt;/strong&gt;, not calls in general. If you're running classification through the cheapest tier available, a local server will essentially never pay for itself - and that's a correct, useful conclusion, not a failure.&lt;br&gt;
It also explains why published estimates disagree by an order of magnitude. They're pricing different substitutions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two things that shift the math&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consolidation.&lt;/strong&gt; That fixed $850 doesn't serve one workload. A single node can run an internal helpdesk bot, a document analysis pipeline, and a coding assistant at the same time. Three applications at 700K tokens/day each look uneconomical in isolation; together they're 2.1M on one server at the same fixed cost. Fine-tuning works the same way - with a provider, every training run is a separate charge; on your own hardware it's capacity you've already paid for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payback period.&lt;/strong&gt; At a steady 10M+ tokens/day against the mid tier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6lvbegq0qaj0w4bxir5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6lvbegq0qaj0w4bxir5.png" alt=" " width="799" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That lands squarely inside the 6–12 month range usually quoted for 10M+ workloads.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture: hybrid, not migration
&lt;/h2&gt;

&lt;p&gt;Moving everything local is the wrong goal. GPT-5.6, Claude Opus 5, and Gemini 3.1 Pro (as of publication - this list will age before you finish reading) simply aren't available for self-hosting. The question isn't what replaces them. It's what you stop sending to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What goes local&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple, high-volume, and sensitive traffic:&lt;/li&gt;
&lt;li&gt;Ticket classification and routing&lt;/li&gt;
&lt;li&gt;Form and document data extraction&lt;/li&gt;
&lt;li&gt;Boilerplate response generation&lt;/li&gt;
&lt;li&gt;Routine autocomplete and code review in the IDE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A support team handling hundreds of near-identical tickets a day is the archetype: predictable volume, straightforward tasks, and frequently customer PII that can't go anywhere. What counts as "local-grade" has shifted a lot, too - a well-quantized 32B now handles work that needed a 70B two years ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stays on the API&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low volume, high cost of being wrong:&lt;/li&gt;
&lt;li&gt;Complex contract or document analysis&lt;/li&gt;
&lt;li&gt;Unfamiliar codebases&lt;/li&gt;
&lt;li&gt;Genuinely novel architectural decisions&lt;/li&gt;
&lt;li&gt;Anything where multi-step reasoning quality is the product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A legal team reviewing contracts once a week is the archetype. The call is expensive and entirely justified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What determines your savings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Published cases land anywhere from −33% to −83%, and the commonly cited range for teams that reach a hybrid setup is 40–70% versus a fully API-dependent stack. Where you fall inside that spread comes down to two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How much of your traffic is even allowed to leave the perimeter.&lt;/li&gt;
&lt;li&gt;Which tier it was hitting before the move.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Displacing a cheap tier yields a smaller percentage for the same engineering effort - see the arithmetic above. The spread between two projects usually says more about their compliance posture than about their engineering.&lt;/p&gt;
&lt;h2&gt;
  
  
  VRAM: the constraint everything else measures against
&lt;/h2&gt;

&lt;p&gt;The baseline rule: &lt;strong&gt;0.5GB of VRAM per billion parameters at 4-bit quantization&lt;/strong&gt;. If the model doesn't fit entirely in VRAM, the system falls back to CPU and throughput drops 10–100x. The gap between 30 TPS and 1 TPS is the gap between a working system and a broken one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sizing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31bazyma7rlf0aumnen5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31bazyma7rlf0aumnen5.png" alt=" " width="799" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Treat the speed column as a conservative floor for single-stream generation through Ollama. On an optimized serving stack the same model classes go several times faster - see the inference server table below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bandwidth, not TFLOPS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For inference, memory bandwidth decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RTX 5090&lt;/strong&gt; - 1.79 TB/s GDDR7, 32GB, ~213 tok/s on 8B models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTX 4090&lt;/strong&gt; - 24GB, 128 tok/s. The 5090 is 67% ahead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTX 3090&lt;/strong&gt; - still earns its place used, for the same 24GB at a fraction of the price&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CUDA remains the default. AMD (ROCm) and Intel (IPEX-LLM) will run basic inference, but with noticeably more configuration friction: a large share of models, optimized kernels, and serving frameworks are NVIDIA-first or NVIDIA-only, and some models simply won't start outside CUDA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quantization and a starting model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q4_K_M&lt;/strong&gt; is the default. Going from Q8 to Q4 nearly halves memory for quality loss you're unlikely to notice. On newer hardware, AWQ and NVFP4 (Blackwell) push the quality/memory tradeoff further.&lt;br&gt;
A sensible starting model is &lt;strong&gt;Qwen3.5-9B&lt;/strong&gt; (~6.6GB, 262K native context) - it fits on a consumer card with room to spare. Check the actual size of whatever tag you pull before you deploy; it varies meaningfully between quantizations.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol457l6sy21tihh3zj5f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol457l6sy21tihh3zj5f.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - Dev setup: Ollama + Open WebUI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yaml
# docker-compose.yml
services:
  ollama:
    image: ollama/ollama:latest
    ports:
      # Ollama listens on all interfaces by default - bind to loopback,
      # expose it only through a gateway or VPN
      - "127.0.0.1:11434:11434"
    volumes:
      - ollama_models:/root/.ollama   # models survive restarts and image updates
    environment:
      - OLLAMA_MAX_LOADED_MODELS=1    # &amp;gt;1 on a single card almost always means OOM
      - OLLAMA_KEEP_ALIVE=30m         # unload the model after idle
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    ports:
      - "127.0.0.1:3000:8080"
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    volumes:
      - webui_data:/app/backend/data
    depends_on:
      - ollama

volumes:
  ollama_models:
  webui_data:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing before you run this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource floor.&lt;/strong&gt; Budget at least 8GB RAM for containers and 50+ GB of disk for models. Ollama detects hardware on its own: GPU if there is one, CPU if there isn't, just slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The migration lever.&lt;/strong&gt; Ollama exposes an OpenAI-compatible API on port 11434. Anything built on the OpenAI SDK switches to the local endpoint by changing one variable. No code changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2 - Production: vLLM&lt;/strong&gt;&lt;br&gt;
Ollama handles up to 4 concurrent requests. That's fine for development and testing. The moment real simultaneous users show up is the moment to move to vLLM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8aliyowrajcq6h1al4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8aliyowrajcq6h1al4k.png" alt=" " width="799" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those TPS ranges span multiple model sizes and GPU generations - the low end is larger models on an RTX 4090, the high end is 8B on a 5090. Use them to compare tools against each other, not as a benchmark. Measure your own numbers on your own workload.&lt;/p&gt;

&lt;p&gt;A note on the multipliers in marketing material: published PagedAttention figures range from 19x to 24x over HuggingFace Transformers depending on benchmark, model, and dataset. The metric that actually matters is P99 under your real concurrency. A reasonable expectation from vLLM on a typical profile: &lt;strong&gt;under 100ms at 128 concurrent requests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ray Serve and TGI win for teams already living in the corresponding ML stack. Without that stack, they add complexity without a payoff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash
docker run --gpus all --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  -p 127.0.0.1:8000:8000 \
  vllm/vllm-openai:latest \
  --model "$MODEL_REPO" \                # Hugging Face repo id
  --served-model-name local-default \    # stable name for clients, decoupled from repo id
  --host 0.0.0.0 --port 8000 \
  --api-key "$VLLM_API_KEY" \            # basic auth
  --quantization awq \                   # only if you actually have AWQ weights
  --gpu-memory-utilization 0.90 \        # 0.95+ hits OOM on KV-cache spikes
  --max-model-len 16384 \                # the main lever on VRAM consumption
  --max-num-seqs 64 \                    # ceiling on concurrent sequences
  --tensor-parallel-size 1                # &amp;gt;1 only for multi-GPU

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three flags people trip over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--ipc=host&lt;/code&gt; &lt;strong&gt;is mandatory.&lt;/strong&gt; Without it, shared memory is capped at the default 64MB and the process dies during batching.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--gpu-memory-utilization&lt;/code&gt; &lt;strong&gt;counts against total card memory,&lt;/strong&gt; not free memory. If anything else is already resident on the GPU, dial it down manually.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--max-num-seqs&lt;/code&gt; &lt;strong&gt;is your real concurrency ceiling.&lt;/strong&gt; vLLM removes the hard 4-request limit, but concurrency isn't unbounded - it's this flag plus whatever VRAM is left for KV-cache. Capacity planning starts there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3 - The router&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The simplest router that works is a static map from task type to model. The temptation to put a classifier in front of inference is strong, but a classifier is one more model call, one more source of latency, and one more thing that can fail. A static map can be debugged with grep and never lies to you.&lt;/p&gt;

&lt;p&gt;The more consequential decision is &lt;strong&gt;what happens when the local instance goes down&lt;/strong&gt;, and one policy for all traffic doesn't work here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ordinary tasks&lt;/strong&gt; fail over to the API. A slightly slower response, and that's the end of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulated traffic&lt;/strong&gt; fails hard - queue it or return 503. 
Automatically falling back to an external API with sensitive data is precisely the incident this whole architecture was built to prevent.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python
import os
import httpx
from openai import (
    OpenAI,
    APIConnectionError,
    APITimeoutError,
    InternalServerError,
    RateLimitError,
)

LOCAL = OpenAI(
    base_url="http://127.0.0.1:8000/v1",
    api_key=os.environ["VLLM_API_KEY"],
    timeout=httpx.Timeout(30.0, connect=2.0),
    max_retries=1,
)
# Example uses an OpenAI-compatible provider. For Claude or Gemini,
# use that vendor's SDK or a proxy layer with an OpenAI-compatible endpoint.
FRONTIER = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

LOCAL_TASKS = {"classify", "extract", "summarize_ticket", "autocomplete"}

LOCAL_MODEL = os.environ["LOCAL_MODEL"]        # must match --served-model-name
FRONTIER_MODEL = os.environ["FRONTIER_MODEL"]

# Deliberately narrow: connection, timeout, 5xx, 429.
# A 400 or 401 is a bug in our config, not a local outage - let it surface.
TRANSIENT = (APITimeoutError, APIConnectionError, InternalServerError, RateLimitError)


def route(task: str, messages: list, regulated: bool = False):
    # `regulated` is set by the calling service: PCI-DSS for payment data,
    # HIPAA for PHI. Different rules, one thing in common:
    # this traffic does not leave the perimeter.
    if not (regulated or task in LOCAL_TASKS):
        return FRONTIER.chat.completions.create(model=FRONTIER_MODEL, messages=messages)

    try:
        return LOCAL.chat.completions.create(model=LOCAL_MODEL, messages=messages)
    except TRANSIENT:
        if regulated:
            raise          # no fallback: a refusal beats a leak
        return FRONTIER.chat.completions.create(model=FRONTIER_MODEL, messages=messages)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catching a bare &lt;code&gt;Exception&lt;/code&gt; here would defeat the point - a config error would quietly route to the API disguised as a local outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that will bite you
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwqdyln9qom3icpus33x1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwqdyln9qom3icpus33x1.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent CPU fallback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ollama doesn't crash when a model won't fit in VRAM. It offloads layers to CPU and starts serving 1 TPS instead of 30. Alerts stay quiet because the service is technically healthy, so the first signal you get is users complaining.&lt;br&gt;
Check after every model update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash
ollama ps    # PROCESSOR column should read "100% GPU", not "47%/53% CPU/GPU"
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Context window isn't free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Doubling the window barely dents throughput but does cost memory: the KV-cache grows linearly with context length, so 8K→16K doubles it. As a share of total consumption, that's typically &lt;strong&gt;+15–20% for a 9B-class model with Q4 weights&lt;/strong&gt; - but run the numbers for your own model, because the fraction changes with model size and window length.&lt;/p&gt;

&lt;p&gt;Maxing out &lt;code&gt;--max-model-len&lt;/code&gt; and getting a silent CPU fallback is a well-established way to kill production on a Friday evening. Time is better spent on RAG (pgvector, Weaviate, Elasticsearch): memory stays predictable and the retrieved context is more relevant than a bloated window ever is. A smaller model with a solid pipeline routinely beats a larger one with no context - faster, and on cheaper hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions in the vector store&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The common mistake is treating it as one shared index for all users. A query from one user legitimately surfaces another user's confidential document, and technically no rule was broken. "Who can retrieve which document" is a question to answer before deployment, not after.&lt;/p&gt;

&lt;p&gt;The fix is document-level permissions, in one of two shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate indexes per role, or&lt;/li&gt;
&lt;li&gt;Filtering retrieval results by the requesting user's rights before context reaches the model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MoE math doesn't work like dense math&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The temptation is to size VRAM by active parameters. A 235B MoE with 22B active costs roughly what a dense 22B costs to run - but every expert weight still has to be resident in memory. The win is quality per unit of compute, not memory savings.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Network isolation&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
The inference server shouldn't be reachable from outside without a specific reason: private subnet, access through an internal gateway or VPN. Ollama listening on all interfaces by default is convenient for local testing and unacceptable in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to self-host
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F022gdihj14eg02fj5cg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F022gdihj14eg02fj5cg9.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Being honest here is more useful than being enthusiastic. Three scenarios where self-hosting clearly loses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Low or unpredictable volume.&lt;/strong&gt; Below 1M tokens/day the API is cheaper at every tier - the infrastructure never pays for itself. With spiky traffic you have to size the local server for peak, which means it idles most of the time. APIs don't have that problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need frontier models.&lt;/strong&gt; The gap between top proprietary models and the best open weights keeps narrowing, but for genuinely hard multi-step reasoning it's still there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No MLOps expertise.&lt;/strong&gt; Self-hosted inference isn't set-and-forget. Models need updating, CUDA errors arrive at the worst possible moment, and performance degrades as load patterns shift. Without someone who can handle that, maintenance overhead eats the token savings faster than you'd expect - and in the $850 breakdown above, that line item is more than half the total.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Open question
&lt;/h2&gt;

&lt;p&gt;The most debatable piece of this setup is the router itself. A static map is transparent and free, but it can't tell a simple ticket from a hard one inside the same category, so some requests go local when they'd be better off escalated.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;how do you split traffic between local and frontier models?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardcoded rules?&lt;/li&gt;
&lt;li&gt;A dedicated router model at the front door?&lt;/li&gt;
&lt;li&gt;Confidence-based escalation, where the local LLM hands off on its own?
And at your volumes, does the classifier overhead actually pay for itself?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Full TCO breakdown, quantization format comparison, RAG and fine-tuning architectures, and the complete breakeven tables are in the &lt;a href="https://alpacked.io/blog/self-hosted-llm-guide/" rel="noopener noreferrer"&gt;full self-hosted LLM guide&lt;/a&gt;, which this post is drawn from.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>llm</category>
      <category>selfhosted</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Our Experience Migrating from AWS ECS to EKS: 4x Faster Releases on a 1-Day SDLC</title>
      <dc:creator>Alpacked</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:34:43 +0000</pubDate>
      <link>https://dev.to/alpacked/our-experience-migrating-from-aws-ecs-to-eks-4x-faster-releases-on-a-1-day-sdlc-1g7k</link>
      <guid>https://dev.to/alpacked/our-experience-migrating-from-aws-ecs-to-eks-4x-faster-releases-on-a-1-day-sdlc-1g7k</guid>
      <description>&lt;p&gt;This is a technical breakdown of a migration we ran for a global B2B service (strict NDA, so no names). The setup was unusual: a release cycle of one day or less, no CTO or architect on the client side, a junior dev team, and acceptable downtime measured in minutes. Our side: two part-time DevOps engineers.&lt;/p&gt;

&lt;p&gt;The short business version with numbers lives &lt;a href="https://alpacked.io/case-studies/eks-migration-4x-releases-cost-savings/" rel="noopener noreferrer"&gt;in the original case study&lt;/a&gt;. Here's the engineering side: what was breaking, what stack we put together, and where things bit us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem &amp;amp; Stack
&lt;/h2&gt;

&lt;p&gt;The starting point was a legacy setup on &lt;strong&gt;AWS ECR/ECS&lt;/strong&gt;. ECS itself isn't the problem – but here everything went wrong at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD pipelines were failing constantly. Every failure blocked developers, and at "release once a day" velocity, that meant hours of patching instead of shipping features.&lt;/li&gt;
&lt;li&gt;Zero observability – troubleshooting meant log archaeology.&lt;/li&gt;
&lt;li&gt;No fast, safe rollback: reverting was manual and terrifying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context made it worse: the client had burned through 8 DevOps teams in the six months before we came on board. The problem wasn't just the technology – no technical owner was making foundational decisions. All communication ran through the CEO and CMO, with dozens of micro-calls per day.&lt;/p&gt;

&lt;p&gt;The classic dilemma at kick-off (Discovery): &lt;strong&gt;quick win vs. rewrite&lt;/strong&gt; – patch the existing ECR/ECS and get to go-live faster, or rewrite everything from scratch on &lt;strong&gt;Amazon EKS&lt;/strong&gt;. We made the case that running on the old architecture from day one put production stability at risk, along with the trust of the earliest users. The client decided not to take that risk.&lt;/p&gt;

&lt;p&gt;The key decision that took 90% of the stress off the table: &lt;strong&gt;we stood up EKS in parallel with the old system&lt;/strong&gt; and only cut over once the new environment was ready – no "big bang" at 2 am. Traffic was shifted gradually at the load balancer/DNS level: start with a small percentage on the new EKS, monitor error rate and latency in Datadog, and scale up to 100%. The old ECS stayed as a hot fallback the whole time; we only shut it down after several days of stable 100% traffic on the new setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjerc00iyxmm1vrvgikun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjerc00iyxmm1vrvgikun.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS instead of ECS.&lt;/strong&gt; We needed an ecosystem for GitOps, fine-grained node control, and service mesh – on ECS, that means workarounds or third-party tools. The foundation: &lt;strong&gt;2 isolated EKS clusters.&lt;/strong&gt; Splitting environments gives you blast radius isolation and cleaner access control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mongo Atlas via VPC Peering.&lt;/strong&gt; State lives in Mongo Atlas; we set up &lt;strong&gt;VPC Peering&lt;/strong&gt; between our VPC and the Atlas VPC – database traffic goes over a private channel, not the public internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Karpenter instead of Cluster Autoscaler.&lt;/strong&gt; FinOps was critical here, and Karpenter gives you mixed node groups (Spot + On-Demand) and bin-packing out of the box. It provisions nodes against actual pod req/limits rather than pre-sliced ASGs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Istio instead of bare Ingress.&lt;/strong&gt; We needed mTLS between services, traffic control, and mesh-level observability – all in one layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ArgoCD instead of CI push-deploys.&lt;/strong&gt; GitOps gives you exactly what we were building toward: rollback in git = rollback in the cluster.&lt;br&gt;
&lt;strong&gt;Datadog APM instead of Prometheus/Grafana.&lt;/strong&gt; We deliberately went against the default here – more on that below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation Details &amp;amp; Code Blocks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. IaC: Everything Through Terraform, Zero Manual Clicks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All infrastructure is code: no &lt;code&gt;kubectl apply&lt;/code&gt; by hand in production, everything goes through Terraform and ArgoCD. We'll skip the basic cluster skeleton (standard &lt;code&gt;terraform-aws-modules/eks/aws&lt;/code&gt;) and focus on &lt;strong&gt;IRSA&lt;/strong&gt; (IAM Roles for Service Accounts). Without proper IRSA, neither Karpenter nor External Secrets can reach the AWS API with least-privilege access – and dropping credentials into pod env vars in production under ISO 27001 isn't an option.&lt;br&gt;
The role for Karpenter, bound to its service account via the cluster's OIDC provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# example for this post: real ARNs/policies are broader&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"karpenter_assume"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Federated"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oidc_provider_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${module.eks.oidc_provider}:sub"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"system:serviceaccount:karpenter:karpenter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"karpenter"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"karpenter-controller"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;karpenter_assume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern (&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; + a condition scoped to a specific &lt;code&gt;serviceaccount&lt;/code&gt;) we used for External Secrets as well – this is exactly the kind of thing that gets credited during security hardening for ISO 27001.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Karpenter: Mixed Spot/On-Demand + Graviton
&lt;/h2&gt;

&lt;p&gt;The tastiest part from a FinOps perspective. We handed node management entirely to Karpenter, let it mix Spot with On-Demand, and moved to &lt;strong&gt;Graviton ARM64 (r8g)&lt;/strong&gt; – roughly 20% cheaper than x86. The key in the &lt;code&gt;NodePool&lt;/code&gt; is getting the requirements right; otherwise, Karpenter either only picks expensive instances or dumps critical workloads onto Spot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requirements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/capacity-type&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spot"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on-demand"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# mixed&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.k8s.aws/instance-family&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r8g"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;                  &lt;span class="c1"&gt;# r8g = Graviton ARM64, specifying arch is redundant&lt;/span&gt;
  &lt;span class="na"&gt;disruption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;consolidationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WhenEmptyOrUnderutilized&lt;/span&gt;   &lt;span class="c1"&gt;# bin-packing&lt;/span&gt;
    &lt;span class="na"&gt;consolidateAfter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200"&lt;/span&gt;   &lt;span class="c1"&gt;# example for this post, real limit depends on your workload profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;consolidationPolicy: WhenEmptyOrUnderutilized&lt;/code&gt; is the bin-packing that trimmed our node count by 30–35%: Karpenter periodically repacks pods onto fewer nodes and terminates the rest. We intentionally skip the &lt;code&gt;arch (arm64)&lt;/code&gt; requirement: the &lt;code&gt;r8g&lt;/code&gt; family only exists on Graviton, so Karpenter infers the architecture from &lt;code&gt;instance-family&lt;/code&gt;. A redundant constraint just clutters the manifest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall #1.&lt;/strong&gt; Spot nodes die without warning. On a stateful workload or long-lived connections without graceful shutdown, you'll see 5xx on every interruption. Fix it with &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;, &lt;code&gt;PodDisruptionBudget&lt;/code&gt;, and pinning critical pods to On-Demand via &lt;code&gt;nodeAffinity&lt;/code&gt;. Don't throw everything onto Spot just because it's cheap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i1l43t3yyh9s95kg2q4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i1l43t3yyh9s95kg2q4.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ArgoCD: Rollback = git revert
&lt;/h2&gt;

&lt;p&gt;GitOps delivered the biggest qualitative leap: deployment and rollback became git operations, not cluster operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git@github.com:client/k8s-manifests.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/payment-api&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# manual changes in the cluster get rolled back to git state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;selfHeal: true&lt;/code&gt; is both a safety net and a pitfall. Safety net: any manual change in the cluster (that classic &lt;code&gt;kubectl edit&lt;/code&gt; under incident pressure) automatically reverts to git state. Pitfall: if someone edits a resource by hand to "quickly put out the fire," ArgoCD silently rolls it back – and they have no idea why their fix "disappeared." The cure is discipline: everything goes through a PR, and for emergencies, temporarily disable auto-sync on the specific application. Rolling back a bad release now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert &amp;lt;bad-commit-sha&amp;gt;
git push origin main
&lt;span class="c"&gt;# ArgoCD picks up the change and restores the cluster to the previous state in seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what gave us &lt;strong&gt;MTTR −82%&lt;/strong&gt;: rollbacks stopped being late-night incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Istio: mTLS and Traffic Control
&lt;/h2&gt;

&lt;p&gt;Service mesh covered mTLS between services and traffic control in one layer. Enforcing strict mTLS at the namespace level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PeerAuthentication&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mtls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STRICT&lt;/span&gt;   &lt;span class="c1"&gt;# encrypted traffic between pods only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pitfall #2.&lt;/strong&gt; Enabling &lt;code&gt;STRICT&lt;/code&gt; mTLS cluster-wide will immediately take down any services not yet in the mesh. Roll it out namespace by namespace, starting with &lt;code&gt;PERMISSIVE&lt;/code&gt;, and on two isolated clusters, do it independently – don't try to flip the switch globally all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why Datadog Instead of Prometheus/Grafana
&lt;/h2&gt;

&lt;p&gt;The most counterintuitive call here. The default DevOps instinct is Prometheus + Grafana (we support both, plus OpenTelemetry). But given the context – junior team, no CTO, relentless pace – the value of a tool isn't its raw power; it's how fast &lt;strong&gt;a developer with no DevOps background&lt;/strong&gt; can find the root cause on their own. Datadog APM gave us exactly the developer-facing interface where a junior opens a trace and immediately sees which service is dragging, no PromQL spelunking required. A deliberate trade-off: slightly more money for SaaS in exchange for a significant drop in troubleshooting overhead. For this client, it paid off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall #3.&lt;/strong&gt; Secrets sprawl across Helm charts fast. We killed that immediately with &lt;strong&gt;External Secrets Operator + AWS Secrets Manager&lt;/strong&gt;, and handed TLS certificates to &lt;code&gt;cert-manager&lt;/code&gt;. Secrets pull from Secrets Manager into the cluster declaratively, no plaintext in git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExternalSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment-db&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
  &lt;span class="na"&gt;secretStoreRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-secrets-manager&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterSecretStore&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment-db-creds&lt;/span&gt;   &lt;span class="c1"&gt;# ESO drops a ready k8s Secret here&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo-uri&lt;/span&gt;
      &lt;span class="na"&gt;remoteRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod/payment/mongo-uri&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, a month later, you have secrets in three places, and nobody knows which one is current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security as a Stack, Not a Checkbox
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ISO 27001&lt;/strong&gt; at the end isn't marketing – it's the result of a specific stack. On the perimeter: &lt;strong&gt;CloudFront (CDN)&lt;/strong&gt; and &lt;strong&gt;AWS WAF&lt;/strong&gt; filtering traffic before it reaches the cluster; inside, &lt;strong&gt;SecurityHub&lt;/strong&gt; and &lt;strong&gt;Inspector&lt;/strong&gt; run continuous vulnerability monitoring (CVE scanning in images, misconfiguration detection in AWS resources). Access is controlled via RBAC, external traffic comes in through the AWS Load Balancer Controller (ALB/NLB), and at the pod level – IRSA with least-privilege, mTLS via Istio, and secrets via ESO. It's the combination, not any single tool, that got the client through ISO 27001 and laid the foundation for SOC 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Culture: Why Infrastructure Won't Save You Without Code Review
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8336t2880m4fwjjvgsty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8336t2880m4fwjjvgsty.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't about YAML, but technically, it saved production more than anything else. With no architect and no CTO, we introduced strict, close code review on every PR – that alone cut hours of patching and downtime. The lesson I'd carve in stone: if you're joining a project with no technical owner, push the client to hire an architect from day one. Until then, code review is your only gatekeeper.&lt;/p&gt;

&lt;p&gt;Separately – close collaboration with the &lt;strong&gt;client's AI team&lt;/strong&gt;. The validation service is tied directly to their ML models, and coordinating at the infrastructure level (right resources for inference, workload isolation, control over what deploys and when) saved the business from real reputational damage more than once. The takeaway: when infrastructure serves ML workloads, DevOps can't live in a vacuum from the people writing the models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering Metrics
&lt;/h2&gt;

&lt;p&gt;What we got in numbers after cutting over to the new setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment frequency ×4&lt;/strong&gt; – through full GitOps automation via ArgoCD.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;MTTR −82% *&lt;/em&gt;– rollback via git revert, zero-downtime deploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute costs: up to −80%&lt;/strong&gt; on Spot instances via Karpenter; &lt;strong&gt;−30–35%&lt;/strong&gt; node count reduction via auto-consolidation; &lt;strong&gt;~−20%&lt;/strong&gt; from moving to Graviton ARM64. Combined: roughly &lt;strong&gt;$45,600/year&lt;/strong&gt; in savings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;99.7% uptime&lt;/strong&gt;, Change Failure Rate down 4%.&lt;/li&gt;
&lt;li&gt;Infrastructure went through full security hardening, earning the client &lt;strong&gt;ISO 27001&lt;/strong&gt; and a foundation for SOC 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The non-technical takeaway: infrastructure went from a blocker to a driver. Developers stopped waiting on deploys and stopped fearing rollbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;The most controversial call in this project, for me, was going with Datadog over the usual Prometheus/Grafana stack – purely for the junior team's sake. It works, but it's vendor lock-in, and it costs money.&lt;/p&gt;

&lt;p&gt;Question for the readers: &lt;strong&gt;how do you balance self-hosted observability (Prometheus/Grafana/OTel) vs. SaaS like Datadog when the people actually staring at dashboards are junior? Do you push for self-hosted for control and cost, or do you trade interface convenience for faster MTTR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And for anyone running Spot through Karpenter in production: what's your real interruption rate, and how do you handle stateful workloads? Would love to compare numbers.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>aws</category>
      <category>finops</category>
    </item>
  </channel>
</rss>
