<?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: Aleksei Aleinikov</title>
    <description>The latest articles on DEV Community by Aleksei Aleinikov (@aleksei_aleinikov).</description>
    <link>https://dev.to/aleksei_aleinikov</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%2F2950325%2F67548842-b25f-4c39-84ef-bc470e23aa8d.jpg</url>
      <title>DEV Community: Aleksei Aleinikov</title>
      <link>https://dev.to/aleksei_aleinikov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksei_aleinikov"/>
    <language>en</language>
    <item>
      <title>vLLM vs Ollama: Which Inference Server You Actually Need in 2026</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Tue, 15 Sep 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/vllm-vs-ollama-which-inference-server-you-actually-need-in-2026-197e</link>
      <guid>https://dev.to/aleksei_aleinikov/vllm-vs-ollama-which-inference-server-you-actually-need-in-2026-197e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" alt="vLLM vs Ollama: Which Inference Server You Actually Need in 2026" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ollama run llama3&lt;/code&gt; gets a model answering questions in about thirty seconds, on a laptop, with zero configuration. That is a genuinely remarkable thing to be able to say about a large language model in 2026, and it is also completely irrelevant to a different question: what happens when 200 people hit that same model at once?&lt;/p&gt;

&lt;p&gt;Those are two different engineering problems, and most “vLLM vs Ollama” content collapses them into one, as if the two projects were competing for the same job. They aren’t. This article covers what each one actually optimizes for, the two ideas — PagedAttention and continuous batching — that explain the entire performance gap under load, and a straight answer to which one you need for the workload you actually have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" alt="Running a model is easy. Serving 200 concurrent users is not — Ollama’s single-request lane compared with vLLM’s packed GPU batching lane" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ollama Actually Is
&lt;/h2&gt;

&lt;p&gt;Ollama is a thin, well-designed operational layer over &lt;strong&gt;llama.cpp&lt;/strong&gt; — Georgi Gerganov’s C/C++ inference engine, the same engine that made running LLaMA-family models on ordinary laptops practical in the first place. Ollama adds the parts that make it feel like a product rather than a research project: a CLI (&lt;code&gt;ollama run llama3&lt;/code&gt;), a REST API, a model library of pre-quantized &lt;strong&gt;GGUF&lt;/strong&gt; files you can pull by name, and a &lt;code&gt;Modelfile&lt;/code&gt; format for customizing prompts and parameters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpiqj7xgpzgrrete0rjnj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpiqj7xgpzgrrete0rjnj.webp" alt="Diagram of how Ollama serves a request: ollama run pulls a GGUF model once, llama.cpp runs the inference on CPU or GPU offload, and returns one streamed response with no request queue or multi-tenant scheduler" width="799" height="493"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;No batching queue exists to configure — which is exactly why there is nothing to tune when a second request shows up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That simplicity is the entire point, not a limitation to apologize for. llama.cpp can offload as many transformer layers to GPU as will fit (Metal on Apple Silicon, CUDA, ROCm, Vulkan) and run the rest on CPU, which is why Ollama runs acceptably on hardware that has no business running a modern LLM at all. It was designed around one machine, one user at a time, and it does that job better than almost anything else available.&lt;/p&gt;

&lt;p&gt;What it was not designed around is a &lt;strong&gt;request scheduler&lt;/strong&gt;. Ollama does queue concurrent requests, but there is no mechanism comparable to what’s described next — no iteration-level batching, no purpose-built memory manager for many simultaneous KV caches. That isn’t a bug; the entire design brief was “run a model with essentially no setup,” and every one of those decisions is correct for that brief.&lt;/p&gt;
&lt;h2&gt;
  
  
  What vLLM Actually Is
&lt;/h2&gt;

&lt;p&gt;vLLM comes out of a research paper, not a product roadmap: &lt;strong&gt;“Efficient Memory Management for Large Language Model Serving with PagedAttention”&lt;/strong&gt; (Kwon et al., UC Berkeley, published at SOSP 2023). The paper’s headline claim — 2-4x higher throughput than serving systems available at the time, such as FasterTransformer and Orca, at the same latency — came from solving one specific, unglamorous problem: GPU memory was being wasted on the &lt;strong&gt;KV cache&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PagedAttention&lt;/strong&gt; is the fix. Every token generated needs the attention mechanism to reference a growing cache of key/value tensors for everything generated so far. Naive serving reserves one contiguous memory block per request, sized for the worst case, which means memory sits unused whenever the real output is shorter — and that unused memory can’t be reassigned to a different request without stopping and reshuffling everything. PagedAttention borrows the idea straight from operating-system virtual memory: split the KV cache into small, fixed-size blocks, allocate them on demand, and reference them indirectly through a block table. Waste shrinks to almost nothing, and — a secondary benefit that matters for things like parallel sampling or beam search — identical prefixes across requests can share the same physical blocks via copy-on-write.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3y9wiycln6cw86brksf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3y9wiycln6cw86brksf.webp" alt="Diagram of vLLM continuous batching: two GPU slots run Request A then Request D back-to-back, and Request B then C then E, with a new request filling a freed slot the instant the previous one finishes rather than waiting for the whole batch" width="800" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Static batching waits for the slowest member of the batch. Continuous batching never lets a freed slot sit idle.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The second piece is &lt;strong&gt;continuous batching&lt;/strong&gt; — sometimes called iteration-level scheduling, an idea introduced by &lt;strong&gt;Orca&lt;/strong&gt; (Yu et al., OSDI 2022) before vLLM productionized it at scale. Older serving systems form a batch, run every request in it to completion, and only then start the next batch — so a single long-running request holds the whole GPU batch hostage while shorter requests finish and their slots sit empty. Continuous batching schedules at the level of individual generation steps: the instant one sequence in a batch finishes, its slot is handed to the next waiting request, on the very next iteration. The GPU stays saturated instead of idling on stragglers.&lt;/p&gt;

&lt;p&gt;Put those two together and the result isn’t “vLLM makes the model smarter or faster per token” — it’s “vLLM keeps far more concurrent requests in flight on the same hardware, with less memory wasted and less GPU sitting idle.” That is a throughput story, and it is the entire reason vLLM exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting Started With Each
&lt;/h2&gt;

&lt;p&gt;Neither of these takes more than a few minutes to try, and running both side by side is the fastest way to feel the difference this article describes rather than just read about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ollama&lt;/strong&gt; — already shown above, one command pulls and runs a quantized model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull llama3
ollama run llama3 &lt;span class="s2"&gt;"Explain PagedAttention in one sentence"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;vLLM&lt;/strong&gt; exposes an OpenAI-compatible API server out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm
vllm serve mistralai/Mistral-7B-Instruct-v0.2
&lt;span class="c"&gt;# now POST to http://localhost:8000/v1/chat/completions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;That single &lt;code&gt;vllm serve&lt;/code&gt; command already gives you continuous batching and PagedAttention — there’s no separate flag to turn them on; they’re the engine’s default request path, not an opt-in mode. If you’re wiring the result into agent tooling — &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/ai/mcp-servers-explained-build-and-run-safely-2026" rel="noopener noreferrer"&gt;MCP servers&lt;/a&gt; or a coding assistant like the ones compared in &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/ai/ai-coding-agents-2026-claude-code-vs-codex-vs-opencode" rel="noopener noreferrer"&gt;AI Coding Agents in 2026&lt;/a&gt; — that OpenAI-compatible endpoint is exactly what makes either engine a drop-in replacement for a hosted API, without touching client code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Trade-off
&lt;/h2&gt;

&lt;p&gt;Neither engine is “better.” They’re optimized for different points on the same curve, and the decision comes down to one question: &lt;strong&gt;is your bottleneck the number of concurrent requests, or the operational complexity of running a GPU fleet?&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faerrsipz11zws519yzpq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faerrsipz11zws519yzpq.webp" alt="Decision matrix for choosing between Ollama and vLLM across five scenarios: prototyping, internal tools, customer-facing APIs, multi-GPU fleets, and edge devices" width="799" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Most serious projects don't pick one forever — they start on Ollama and graduate to vLLM once real concurrency shows up.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prototyping and internal tools with a handful of users&lt;/strong&gt; — Ollama wins on setup cost alone. There is no batching behavior to reason about because there is essentially no contention to manage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A customer-facing API serving many concurrent users&lt;/strong&gt; — this is the scenario PagedAttention and continuous batching exist for. The gap between the two engines widens as concurrency rises; it is close to invisible at 1 request and large at 50.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A multi-GPU fleet where cost per token matters&lt;/strong&gt; — vLLM supports tensor and pipeline parallelism to spread a large model across GPUs, often on a managed Kubernetes GPU node pool (see &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/cloud/gke-autopilot-vs-standard-2026" rel="noopener noreferrer"&gt;GKE Autopilot vs Standard&lt;/a&gt; for how that scheduling choice plays out for GPU workloads specifically), and higher achievable GPU utilization translates directly into fewer GPUs needed for the same load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge devices or environments with no dedicated GPU ops team&lt;/strong&gt; — Ollama’s CPU/GPU-offload flexibility and minimal footprint are the right fit; vLLM’s value proposition assumes GPU infrastructure you’re actively managing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quantization is where the two projects diverge in a way worth knowing before you commit to one. Ollama’s GGUF models (K-quants like Q4_K_M) are built for the CPU/GPU-split use case and are covered in depth in &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/ai/quantization-explained-run-70b-model-consumer-hardware-2026" rel="noopener noreferrer"&gt;Quantization Explained: How to Run a 70B Model on Consumer Hardware&lt;/a&gt; — that article’s VRAM math applies directly to sizing an Ollama deployment. vLLM instead leans on GPU-first formats such as AWQ and GPTQ, which tend to be faster when the model fits entirely in VRAM, plus growing FP8 support on newer hardware. If you’re planning to serve a model you’re currently only running locally, that quantization format is one of the first decisions the migration forces on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Else Is in This Space
&lt;/h2&gt;

&lt;p&gt;vLLM and Ollama are the two names that come up most, but they aren’t the only options, and it’s worth knowing where the others sit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text Generation Inference (TGI)&lt;/strong&gt;, from Hugging Face, targets the same production-throughput niche as vLLM with its own continuous-batching implementation and a Rust-based serving core. A reasonable pick if your stack already lives in the Hugging Face ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SGLang&lt;/strong&gt; , from the same research lineage as vLLM, adds structured generation and constrained decoding on top of a similarly optimized backend — relevant if your workload leans heavily on structured output or agentic tool-calling rather than open-ended chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LM Studio&lt;/strong&gt; and &lt;strong&gt;llama.cpp’s own bundled server&lt;/strong&gt; sit closer to Ollama’s niche: local-first, single-user-oriented, with LM Studio adding a desktop GUI on top.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these change the underlying decision. They’re variations on the same two poles — optimized for concurrent throughput, or optimized for zero-friction local use — and the honest answer to “which one” starts with being honest about which of those two problems you actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ollama run llama3&lt;/code&gt; and a production inference endpoint serving 200 people are not the same engineering problem wearing different clothes — they’re genuinely different problems, and conflating them is why “vLLM vs Ollama” arguments so often talk past each other. Ollama’s entire value is refusing to force you to think about batching, scheduling, or GPU memory layout, and for a huge share of real use — prototyping, internal tools, small teams, edge deployment — that’s exactly the right trade to make. vLLM’s entire value is PagedAttention and continuous batching turning GPU memory and scheduling into something you actively manage, because at real concurrency that management is where the throughput comes from.&lt;/p&gt;

&lt;p&gt;Most projects that matter end up using both, just not at the same time: Ollama while you’re figuring out whether the product works at all, vLLM once concurrent users are the thing standing between you and the next order of magnitude of scale. Picking based on which stage you’re actually in, rather than which name sounds more serious, is the whole decision.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/ai/vllm-vs-ollama-inference-server-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vllm</category>
      <category>ollama</category>
      <category>llminference</category>
      <category>inferenceserver</category>
    </item>
    <item>
      <title>vLLM vs. Ollama: Welchen Inference-Server Sie 2026 wirklich brauchen</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Tue, 15 Sep 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/vllm-vs-ollama-welchen-inference-server-sie-2026-wirklich-brauchen-1l3k</link>
      <guid>https://dev.to/aleksei_aleinikov/vllm-vs-ollama-welchen-inference-server-sie-2026-wirklich-brauchen-1l3k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" alt="vLLM vs. Ollama: Welchen Inference-Server Sie 2026 wirklich brauchen" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ollama run llama3&lt;/code&gt; bringt in rund dreißig Sekunden ein antwortendes Modell — auf einem Laptop, ohne jede Konfiguration. Das ist 2026 eine bemerkenswerte Sache, die man über ein großes Sprachmodell sagen kann, und gleichzeitig für eine andere Frage völlig irrelevant: Was passiert, wenn 200 Menschen dasselbe Modell gleichzeitig anfragen?&lt;/p&gt;

&lt;p&gt;Das sind zwei verschiedene Engineering-Probleme, und die meisten “vLLM vs. Ollama”-Inhalte werfen sie in einen Topf, als würden beide Projekte um denselben Job konkurrieren. Tun sie nicht. Dieser Artikel behandelt, worauf jedes der beiden tatsächlich optimiert ist, die zwei Ideen — PagedAttention und Continuous Batching —, die die gesamte Performance-Lücke unter Last erklären, und eine klare Antwort darauf, welches Sie für die Last brauchen, die Sie tatsächlich haben.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jebyzevsmq2vj7bfu9.webp" alt="Ein Modell laufen zu lassen ist einfach. 200 gleichzeitige Nutzer zu bedienen nicht — Ollamas Einzelanfrage-Spur im Vergleich zu vLLMs gepackter GPU-Batching-Spur" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Was Ollama wirklich ist
&lt;/h2&gt;

&lt;p&gt;Ollama ist eine schlanke, gut gestaltete Betriebsschicht über &lt;strong&gt;llama.cpp&lt;/strong&gt; — Georgi Gerganovs C/C++-Inferenz-Engine, dieselbe Engine, die das Ausführen von LLaMA-Familien-Modellen auf gewöhnlichen Laptops überhaupt erst praktikabel gemacht hat. Ollama fügt die Teile hinzu, die es wie ein fertiges Produkt statt ein Forschungsprojekt wirken lassen: eine CLI (&lt;code&gt;ollama run llama3&lt;/code&gt;), eine REST-API, eine Modellbibliothek vorquantisierter &lt;strong&gt;GGUF&lt;/strong&gt; -Dateien, die per Name gezogen werden können, und ein &lt;code&gt;Modelfile&lt;/code&gt;-Format zum Anpassen von Prompts und Parametern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpiqj7xgpzgrrete0rjnj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpiqj7xgpzgrrete0rjnj.webp" alt="Diagramm, wie Ollama eine Anfrage bedient: ollama run zieht einmalig ein GGUF-Modell, llama.cpp führt die Inferenz auf CPU oder mit GPU-Offload aus und liefert eine gestreamte Antwort ohne Warteschlange oder Multi-Tenant-Scheduler" width="799" height="493"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Es gibt keine Batching-Warteschlange zu konfigurieren — genau deshalb gibt es nichts zu tunen, sobald eine zweite Anfrage eintrifft.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Diese Einfachheit ist der ganze Sinn der Sache, keine Einschränkung, für die man sich entschuldigen müsste. llama.cpp kann so viele Transformer-Layer wie möglich auf die GPU auslagern (Metal auf Apple Silicon, CUDA, ROCm, Vulkan) und den Rest auf der CPU ausführen — deshalb läuft Ollama akzeptabel auf Hardware, die eigentlich gar kein modernes LLM ausführen sollte. Es wurde für eine Maschine, einen Nutzer zur Zeit entworfen, und diese Aufgabe erledigt es besser als fast alles andere Verfügbare.&lt;/p&gt;

&lt;p&gt;Wofür es nicht entworfen wurde, ist ein &lt;strong&gt;Anfrage-Scheduler&lt;/strong&gt;. Ollama reiht gleichzeitige Anfragen durchaus in eine Warteschlange ein, aber es gibt keinen Mechanismus vergleichbar mit dem, was als Nächstes beschrieben wird — kein Iteration-Level-Batching, keinen zweckgebauten Speichermanager für viele gleichzeitige KV-Caches. Das ist kein Fehler; das gesamte Design-Briefing lautete “ein Modell mit praktisch keinem Setup laufen lassen”, und jede dieser Entscheidungen ist für genau dieses Briefing richtig.&lt;/p&gt;
&lt;h2&gt;
  
  
  Was vLLM wirklich ist
&lt;/h2&gt;

&lt;p&gt;vLLM stammt aus einem Forschungspapier, nicht aus einer Produkt-Roadmap: &lt;strong&gt;“Efficient Memory Management for Large Language Model Serving with PagedAttention”&lt;/strong&gt; (Kwon et al., UC Berkeley, veröffentlicht auf der SOSP 2023). Die Kernaussage des Papers — 2- bis 4-fach höherer Durchsatz als damals verfügbare Serving-Systeme wie FasterTransformer und Orca, bei gleicher Latenz — kam aus der Lösung eines konkreten, unglamourösen Problems: GPU-Speicher wurde für den &lt;strong&gt;KV-Cache&lt;/strong&gt; verschwendet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PagedAttention&lt;/strong&gt; ist die Lösung. Jedes generierte Token braucht den Aufmerksamkeitsmechanismus, um auf einen wachsenden Cache von Key/Value-Tensoren für alles bisher Generierte zurückzugreifen. Naives Serving reserviert pro Anfrage einen zusammenhängenden Speicherblock, dimensioniert für den schlechtesten Fall — das heißt, Speicher bleibt ungenutzt, sobald die tatsächliche Ausgabe kürzer ist, und dieser ungenutzte Speicher kann keiner anderen Anfrage zugewiesen werden, ohne alles anzuhalten und neu zu ordnen. PagedAttention übernimmt die Idee direkt aus dem virtuellen Speicher von Betriebssystemen: den KV-Cache in kleine, feste Blöcke aufteilen, sie bei Bedarf zuweisen und indirekt über eine Blocktabelle referenzieren. Die Verschwendung schrumpft auf nahezu null, und — ein Nebeneffekt, der etwa bei parallelem Sampling oder Beam Search zählt — identische Präfixe über mehrere Anfragen hinweg können sich per Copy-on-Write dieselben physischen Blöcke teilen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3y9wiycln6cw86brksf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3y9wiycln6cw86brksf.webp" alt="Diagramm des Continuous Batching von vLLM: Zwei GPU-Slots verarbeiten Anfrage A und danach Anfrage D direkt hintereinander, sowie Anfrage B, dann C, dann E, wobei eine neue Anfrage einen frei gewordenen Slot sofort füllt, statt auf den gesamten Batch zu warten" width="800" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Statisches Batching wartet auf das langsamste Mitglied des Batches. Continuous Batching lässt einen frei gewordenen Slot nie im Leerlauf.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Das zweite Element ist &lt;strong&gt;Continuous Batching&lt;/strong&gt; — manchmal Iteration-Level-Scheduling genannt, eine Idee, die &lt;strong&gt;Orca&lt;/strong&gt; (Yu et al., OSDI 2022) einführte, bevor vLLM sie im großen Maßstab produktionsreif machte. Ältere Serving-Systeme bilden einen Batch, führen jede Anfrage darin bis zum Abschluss aus und starten erst danach den nächsten Batch — eine einzelne lang laufende Anfrage hält damit den gesamten GPU-Batch als Geisel, während kürzere Anfragen fertig sind und ihre Slots leer bleiben. Continuous Batching plant auf Ebene einzelner Generierungsschritte: Sobald eine Sequenz in einem Batch fertig ist, wird ihr Slot in der nächsten Iteration sofort der nächsten wartenden Anfrage übergeben. Die GPU bleibt ausgelastet, statt auf Nachzügler zu warten.&lt;/p&gt;

&lt;p&gt;Kombiniert man beides, lautet das Ergebnis nicht “vLLM macht das Modell klüger oder pro Token schneller” — sondern “vLLM hält deutlich mehr gleichzeitige Anfragen auf derselben Hardware in Bearbeitung, mit weniger verschwendetem Speicher und weniger GPU-Leerlauf.” Das ist eine Durchsatz-Geschichte, und das ist der gesamte Grund, warum es vLLM gibt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Erste Schritte mit beiden
&lt;/h2&gt;

&lt;p&gt;Keines von beiden braucht mehr als ein paar Minuten zum Ausprobieren, und beide nebeneinander laufen zu lassen ist der schnellste Weg, den Unterschied aus diesem Artikel selbst zu spüren statt nur darüber zu lesen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ollama&lt;/strong&gt; — bereits oben gezeigt, ein Befehl zieht ein quantisiertes Modell und startet es:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull llama3
ollama run llama3 &lt;span class="s2"&gt;"Erkläre PagedAttention in einem Satz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;vLLM&lt;/strong&gt; bringt von Haus aus einen OpenAI-kompatiblen API-Server mit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm
vllm serve mistralai/Mistral-7B-Instruct-v0.2
&lt;span class="c"&gt;# jetzt POST an http://localhost:8000/v1/chat/completions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Schon dieser einzelne &lt;code&gt;vllm serve&lt;/code&gt;-Befehl liefert Continuous Batching und PagedAttention mit — es gibt kein separates Flag, um sie einzuschalten; sie sind der Standard-Anfragepfad der Engine, kein Opt-in-Modus. Wer das Ergebnis in Agenten-Tooling einbindet — &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/ai/mcp-server-erklaert-selbst-bauen-und-sicher-betreiben-2026" rel="noopener noreferrer"&gt;MCP-Server&lt;/a&gt; oder einen Coding-Assistenten wie die in &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/ai/ki-coding-agents-2026-claude-code-vs-codex-vs-opencode" rel="noopener noreferrer"&gt;KI-Coding-Agents 2026&lt;/a&gt; verglichenen — profitiert genau von diesem OpenAI-kompatiblen Endpunkt: Er macht beide Engines zu einem Drop-in-Ersatz für eine gehostete API, ohne Client-Code anzufassen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Der eigentliche Trade-off
&lt;/h2&gt;

&lt;p&gt;Keine der beiden Engines ist “besser”. Sie sind für unterschiedliche Punkte auf derselben Kurve optimiert, und die Entscheidung läuft auf eine Frage hinaus: &lt;strong&gt;Ist Ihr Engpass die Anzahl gleichzeitiger Anfragen, oder die operative Komplexität, eine GPU-Flotte zu betreiben?&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faerrsipz11zws519yzpq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faerrsipz11zws519yzpq.webp" alt="Entscheidungsmatrix für die Wahl zwischen Ollama und vLLM über fünf Szenarien: Prototyping, interne Tools, kundenseitige APIs, Multi-GPU-Flotten und Edge-Geräte" width="799" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Die meisten ernsthaften Projekte entscheiden sich nicht für immer — sie starten mit Ollama und wechseln zu vLLM, sobald echte Nebenläufigkeit auftaucht.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prototyping und interne Tools mit einer Handvoll Nutzer&lt;/strong&gt; — Ollama gewinnt allein durch den Setup-Aufwand. Es gibt praktisch kein Konkurrenzverhalten zu managen, also gibt es auch kein Batching-Verhalten, über das man nachdenken müsste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eine kundenseitige API mit vielen gleichzeitigen Nutzern&lt;/strong&gt; — genau für dieses Szenario existieren PagedAttention und Continuous Batching. Die Lücke zwischen beiden Engines wächst mit steigender Nebenläufigkeit; bei einer Anfrage ist sie kaum sichtbar, bei fünfzig groß.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eine Multi-GPU-Flotte, bei der die Kosten pro Token zählen&lt;/strong&gt; — vLLM unterstützt Tensor- und Pipeline-Parallelismus, um ein großes Modell über mehrere GPUs zu verteilen, oft auf einem verwalteten Kubernetes-GPU-Node-Pool (siehe &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/cloud/gke-autopilot-vs-standard-vergleich-2026" rel="noopener noreferrer"&gt;GKE Autopilot vs. Standard&lt;/a&gt; dafür, wie sich diese Scheduling-Entscheidung speziell bei GPU-Workloads auswirkt), und höhere erreichbare GPU-Auslastung bedeutet direkt weniger benötigte GPUs für dieselbe Last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge-Geräte oder Umgebungen ohne dediziertes GPU-Ops-Team&lt;/strong&gt; — Ollamas Flexibilität beim CPU/GPU-Split und der geringe Footprint sind hier die richtige Wahl; vLLMs Wertversprechen setzt GPU-Infrastruktur voraus, die aktiv betrieben wird.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Die Quantisierung ist der Punkt, an dem sich die beiden Projekte auf eine Weise unterscheiden, die man kennen sollte, bevor man sich festlegt. Ollamas GGUF-Modelle (K-Quants wie Q4_K_M) sind für den CPU/GPU-Split-Anwendungsfall gebaut und werden ausführlich in &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/ai/quantisierung-erklaert-70b-modell-auf-normaler-hardware-2026" rel="noopener noreferrer"&gt;Quantisierung erklärt: 70B-Modell auf normaler Hardware&lt;/a&gt; behandelt — die dortige VRAM-Rechnung gilt direkt für die Dimensionierung eines Ollama-Deployments. vLLM setzt stattdessen eher auf GPU-first-Formate wie AWQ und GPTQ, die tendenziell schneller sind, wenn das Modell vollständig in den VRAM passt, plus wachsende FP8-Unterstützung auf neuerer Hardware. Wer plant, ein bisher nur lokal laufendes Modell zu servieren, stößt auf genau diese Formatfrage als eine der ersten Entscheidungen der Migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wer sonst noch in diesem Feld mitspielt
&lt;/h2&gt;

&lt;p&gt;vLLM und Ollama sind die beiden am häufigsten genannten Namen, aber nicht die einzigen Optionen, und es lohnt sich zu wissen, wo die anderen stehen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text Generation Inference (TGI)&lt;/strong&gt; von Hugging Face zielt mit eigener Continuous-Batching-Implementierung und einem Rust-basierten Serving-Kern auf dieselbe Produktions-Durchsatz-Nische wie vLLM. Eine sinnvolle Wahl, wenn der eigene Stack ohnehin im Hugging-Face-Ökosystem lebt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SGLang&lt;/strong&gt; , aus derselben Forschungslinie wie vLLM, ergänzt strukturierte Generierung und Constrained Decoding auf einem ähnlich optimierten Backend — relevant, wenn die Last stark auf strukturierter Ausgabe oder agentischem Tool-Calling statt offenem Chat liegt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LM Studio&lt;/strong&gt; und &lt;strong&gt;llama.cpps eigener mitgelieferter Server&lt;/strong&gt; liegen näher an Ollamas Nische: lokal-first, auf Einzelnutzer ausgerichtet, wobei LM Studio zusätzlich eine Desktop-GUI mitbringt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keine dieser Optionen ändert die zugrunde liegende Entscheidung. Es sind Variationen derselben zwei Pole — optimiert für gleichzeitigen Durchsatz oder optimiert für reibungslose lokale Nutzung —, und die ehrliche Antwort auf “welches von beiden” beginnt damit, ehrlich zu sein, welches der beiden Probleme man tatsächlich hat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fazit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ollama run llama3&lt;/code&gt; und ein Produktions-Inference-Endpunkt für 200 Nutzer sind nicht dasselbe Engineering-Problem in anderem Gewand — es sind wirklich unterschiedliche Probleme, und sie zu vermischen ist der Grund, warum “vLLM vs. Ollama”-Debatten so oft aneinander vorbeireden. Ollamas gesamter Wert liegt darin, dass man nicht über Batching, Scheduling oder GPU-Speicherlayout nachdenken muss, und für einen großen Teil der realen Nutzung — Prototyping, interne Tools, kleine Teams, Edge-Deployment — ist das genau der richtige Tausch. vLLMs gesamter Wert liegt darin, dass PagedAttention und Continuous Batching GPU-Speicher und Scheduling zu etwas machen, das man aktiv verwaltet, weil bei echter Nebenläufigkeit genau dort der Durchsatz herkommt.&lt;/p&gt;

&lt;p&gt;Die meisten Projekte, die zählen, nutzen am Ende beides — nur nicht gleichzeitig: Ollama, solange man herausfindet, ob das Produkt überhaupt funktioniert, vLLM, sobald gleichzeitige Nutzer das sind, was zwischen einem und der nächsten Größenordnung an Skalierung steht. Die Wahl danach zu treffen, in welcher Phase man tatsächlich ist, statt danach, welcher Name ernsthafter klingt, ist die ganze Entscheidung.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ursprünglich veröffentlicht auf &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/ai/vllm-vs-ollama-inference-server-2026-vergleich" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vllm</category>
      <category>ollama</category>
      <category>llminferenz</category>
      <category>inferenceserver</category>
    </item>
    <item>
      <title>Passkeys vs Passwords: How WebAuthn Actually Works, Who Builds What, and How to Roll It Out</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Mon, 14 Sep 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/passkeys-vs-passwords-how-webauthn-actually-works-who-builds-what-and-how-to-roll-it-out-lff</link>
      <guid>https://dev.to/aleksei_aleinikov/passkeys-vs-passwords-how-webauthn-actually-works-who-builds-what-and-how-to-roll-it-out-lff</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" alt="Passkeys vs Passwords: How WebAuthn Actually Works, Who Builds What, and How to Roll It Out" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A password is a secret two parties have to keep at the same time: you, and every server you ever typed it into. That is the entire design flaw. It does not matter how long or random the string is — if it can be typed, it can be phished, and if it is stored anywhere else, it can be breached and reused.&lt;/p&gt;

&lt;p&gt;A passkey is not a stronger secret. It is not a secret at all, on the server side. It is one half of a key pair, and the half the server gets is mathematically useless to an attacker.&lt;/p&gt;

&lt;p&gt;That distinction gets lost in most “turn on passkeys” marketing copy, which is a shame, because the protocol underneath is genuinely elegant and worth understanding at the message level — especially if you are the one who has to implement it. This article covers how the two core WebAuthn ceremonies actually work, what “synced” versus “device-bound” really means, who ships passkeys today and what they each hold, how to implement support without breaking account recovery, and an honest read on where this is still rough.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" alt="Passwords are a secret you keep sharing. Passkeys replace it with a key pair only your device holds — comparison of password risks vs passkey properties" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Passkey Actually Is
&lt;/h2&gt;

&lt;p&gt;Strip away the product framing and a passkey is a WebAuthn public-key credential, and WebAuthn is a W3C standard that, paired with the FIDO Alliance’s CTAP2 protocol, forms &lt;strong&gt;FIDO2&lt;/strong&gt;. Three parties are involved in every ceremony, and it is worth naming them precisely because the vendor documentation rarely does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The relying party (RP)&lt;/strong&gt; — the website or app that wants to authenticate you. This is your server. It never sees, stores, or transmits a private key at any point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The client&lt;/strong&gt; — the browser or OS, which mediates between the RP’s JavaScript (&lt;code&gt;navigator.credentials&lt;/code&gt;) and the authenticator, and — this part matters — independently verifies the origin before anything is signed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The authenticator&lt;/strong&gt; — the thing that actually holds the private key: a phone’s secure enclave, a laptop’s TPM, or a roaming hardware key (YubiKey, Titan). It never talks to the network directly; it only talks to the client over CTAP2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A password login has two parties and one shared secret. A passkey login has three parties and zero shared secrets — the public key that sits on your server is, by definition, safe to leak. That is the whole point, and everything else in this article is detail on how that property is achieved and where it leaks back into “well, actually, it’s complicated.”&lt;/p&gt;

&lt;h2&gt;
  
  
  How Registration Works
&lt;/h2&gt;

&lt;p&gt;“Adding a passkey” to an account is a WebAuthn &lt;strong&gt;registration ceremony&lt;/strong&gt; (&lt;code&gt;navigator.credentials.create()&lt;/code&gt;). Six messages, three parties, and the private key touches exactly one of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva2yzu72jch8kyj22t97.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva2yzu72jch8kyj22t97.webp" alt="WebAuthn registration ceremony diagram: relying party sends a challenge, the browser calls navigator.credentials.create, the authenticator generates a key pair on-device and returns the public key, and the server stores only the public key" width="800" height="467"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The private key is generated in step 3 and never appears in any of the other five messages.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few details that matter once you actually implement this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The challenge is single-use and server-generated.&lt;/strong&gt; It exists purely to prevent replay — the signature the authenticator eventually produces is only valid for that exact challenge, so a captured registration response cannot be replayed against a different session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attestation is optional and mostly not worth requiring.&lt;/strong&gt; The authenticator can cryptographically prove what make/model it is (&lt;code&gt;attestation: "direct"&lt;/code&gt;), but doing so leaks device fingerprinting information, requires you to maintain a metadata trust store, and most consumer flows use &lt;code&gt;attestation: "none"&lt;/code&gt;. Reach for direct attestation only in regulated/enterprise contexts where you must enforce a specific hardware allow-list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discoverable (resident) credentials are what make “passkey” different from old-style WebAuthn 2FA.&lt;/strong&gt; Setting &lt;code&gt;residentKey: "required"&lt;/code&gt; tells the authenticator to store the credential such that it can be found &lt;em&gt;without&lt;/em&gt; the RP sending a username first — that is what powers the “sign in with a passkey” account picker instead of “type your username, then use your security key.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The credential ID and public key are what you store — never anything else.&lt;/strong&gt; A minimal schema is &lt;code&gt;(user_id, credential_id, public_key_cose, sign_count, transports, created_at)&lt;/code&gt;. &lt;code&gt;sign_count&lt;/code&gt; is a legacy anti-cloning counter from single-device authenticators; most synced passkeys report it as a flat &lt;code&gt;0&lt;/code&gt;, so treat it as advisory, not a security boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Login Works
&lt;/h2&gt;

&lt;p&gt;Logging in is the &lt;strong&gt;authentication ceremony&lt;/strong&gt; (&lt;code&gt;navigator.credentials.get()&lt;/code&gt;), and it is the mirror image of registration — a challenge goes out, a signature comes back, and this time the server has something to check the signature against.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqw5rm5o4raqpf6ac95m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqw5rm5o4raqpf6ac95m.webp" alt="Diagram comparing the same passkey login attempt on the real site yourapp.com versus a look-alike phishing domain: on the real origin the authenticator signs the challenge and a session is created, on the phishing origin the authenticator refuses to sign and there is nothing to steal" width="800" height="467"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Same user, same tap, two different origins — the browser decides the outcome before a signature is ever produced.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The detail that actually delivers the anti-phishing property is easy to skim past: &lt;strong&gt;the client — not the user — checks that the origin requesting the assertion matches the origin the credential was registered to.&lt;/strong&gt; A pixel-perfect clone of your login page hosted on a look-alike domain can render an identical &lt;code&gt;navigator.credentials.get()&lt;/code&gt; call, and the browser will simply refuse to produce a valid signature for it. There is no “type the password into the wrong box” failure mode, because there is no box.&lt;/p&gt;

&lt;p&gt;Two implementation notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conditional UI (autofill) is what makes this feel like a password field.&lt;/strong&gt; Calling &lt;code&gt;get()&lt;/code&gt; with &lt;code&gt;mediation: "conditional"&lt;/code&gt; on page load lets the browser surface saved passkeys in the normal username autofill dropdown, so the login form still &lt;em&gt;looks&lt;/em&gt; like a plain input — it just also offers a one-tap passkey option. Skipping this is the single biggest reason “we added passkeys” rollouts get low adoption: users never see the option because it is hidden behind a separate button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User verification (&lt;code&gt;userVerification: "preferred"&lt;/code&gt; vs &lt;code&gt;"required"&lt;/code&gt;) decides whether a bare “is a device present” check is enough&lt;/strong&gt;, or whether the local biometric/PIN unlock is mandatory. For anything beyond a trivial account, require it — otherwise an unlocked, unattended device becomes a full authentication bypass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who Actually Holds the Private Key
&lt;/h2&gt;

&lt;p&gt;Here is where the “just use passkeys” pitch gets less simple, and it is worth being precise about it because it changes your account-recovery design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7c4xe758igc3lpzp3iy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7c4xe758igc3lpzp3iy.webp" alt="Diagram of passkey sync fabrics in 2026: Apple iCloud Keychain, Google Password Manager, Microsoft Windows Hello and Entra ID, and cross-platform password managers like 1Password and Bitwarden, bridged only by FIDO2 CTAP2 hybrid transport via QR code" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Four sync fabrics, one cross-ecosystem fallback — and it is a transport bridge, not a credential-migration path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synced passkeys&lt;/strong&gt; live inside a platform vendor’s end-to-end-encrypted cloud and follow the user’s account, not their device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple&lt;/strong&gt; syncs through iCloud Keychain across macOS, iOS, iPadOS and visionOS, gated behind the Apple ID and (for viewing) device passcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; syncs through Google Password Manager across Android, ChromeOS and Chrome on any OS, tied to the Google account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft&lt;/strong&gt; anchors platform passkeys to Windows Hello and the Microsoft account; on the workforce side, Entra ID adds policy control over which authenticators are acceptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1Password, Bitwarden, Dashlane&lt;/strong&gt; and other password managers sync passkeys through their own vault, cross-platform by design — the whole point of a password manager is not being locked to one OS vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Device-bound passkeys&lt;/strong&gt; are the other model: the private key is generated inside one specific piece of hardware and is never exported, backed up, or synced anywhere, by design. This is the default (and often the &lt;em&gt;only&lt;/em&gt; option) for FIDO2 hardware security keys, and it is a mode some enterprise MDM policies force even for platform authenticators, trading recoverability for a much narrower attack surface — the key can only ever be exfiltrated by physically compromising that one device.&lt;/p&gt;

&lt;p&gt;What does &lt;strong&gt;not&lt;/strong&gt; yet work cleanly: moving a synced passkey from one vendor’s fabric to another. A passkey created in iCloud Keychain does not appear in Google Password Manager. The practical bridge across ecosystems is &lt;strong&gt;FIDO2 CTAP2 hybrid transport&lt;/strong&gt; — scan a QR code on a laptop, approve on a phone over Bluetooth, and that phone acts as a one-time roaming authenticator for that browser session. It is a genuinely useful fallback for “I’m on a friend’s computer” or a platform gap, but the private key still never leaves the phone, and it is not a migration tool. The FIDO Alliance’s Credential Exchange Protocol (CXP) is the standardization effort aimed at proper cross-vendor export/import; as of 2026 it is still early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Offers What in 2026
&lt;/h2&gt;

&lt;p&gt;For most teams the real decision is not “should we implement raw WebAuthn,” it is “which layer do we implement it at.” Here is the landscape, roughly ordered from consumer platform down to identity infrastructure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Passkey role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apple, Google, Microsoft&lt;/td&gt;
&lt;td&gt;OS / browser platforms&lt;/td&gt;
&lt;td&gt;Ship the authenticator + sync fabric (iCloud Keychain, Google Password Manager, Windows Hello) that end users already have on their device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1Password, Bitwarden, Dashlane&lt;/td&gt;
&lt;td&gt;Password managers&lt;/td&gt;
&lt;td&gt;Store and sync passkeys across any OS/browser as a vault item, independent of any single platform vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta, Auth0, Microsoft Entra ID, Ping Identity&lt;/td&gt;
&lt;td&gt;Enterprise IdPs&lt;/td&gt;
&lt;td&gt;Let you turn on passkey sign-in for your workforce or customer app without touching WebAuthn code directly — the IdP is the relying party&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak, Authentik&lt;/td&gt;
&lt;td&gt;Self-hosted IdPs&lt;/td&gt;
&lt;td&gt;Same idea, open source — see &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/security/is-keycloak-free-keycloak-vs-authentik-2026" rel="noopener noreferrer"&gt;Keycloak vs Authentik&lt;/a&gt; if you are choosing between them; both support WebAuthn as a self-service credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Cognito, Clerk&lt;/td&gt;
&lt;td&gt;Managed app-auth platforms&lt;/td&gt;
&lt;td&gt;Ship WebAuthn/passkey sign-in as a built-in configuration option — no custom ceremony code required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleWebAuthn, &lt;code&gt;webauthn4j&lt;/code&gt;, &lt;code&gt;go-webauthn/webauthn&lt;/code&gt;, py_webauthn&lt;/td&gt;
&lt;td&gt;Open-source relying-party libraries&lt;/td&gt;
&lt;td&gt;What you reach for if you &lt;em&gt;are&lt;/em&gt; building the identity layer yourself — parse/verify the ceremony without hand-rolling CBOR/COSE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yubico, Google (Titan), SoloKeys&lt;/td&gt;
&lt;td&gt;Hardware security keys&lt;/td&gt;
&lt;td&gt;Device-bound roaming authenticators for workforce/high-assurance accounts, independent of any OS sync fabric&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest read: &lt;strong&gt;if your product already sits behind Okta, Auth0, Entra ID, or a self-hosted Keycloak/Authentik instance, turning on passkeys is a configuration change, not an engineering project.&lt;/strong&gt; The IdP is already the relying party; you are enabling a credential type it already knows how to handle. The engineering project described below is for the case where &lt;em&gt;you&lt;/em&gt; are the relying party — you run your own auth stack and want to add passkeys as a first-class credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Implement It
&lt;/h2&gt;

&lt;p&gt;Assume you are the relying party. The shape of the work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Pick a server library — do not parse CBOR/COSE by hand.&lt;/strong&gt; The WebAuthn wire format (attestation objects, COSE public keys, CBOR-encoded authenticator data) is exactly the kind of thing that is easy to get subtly wrong in a way that only shows up as a security bug later. Use a maintained library for your stack: &lt;a href="https://simplewebauthn.dev/" rel="noopener noreferrer"&gt;SimpleWebAuthn&lt;/a&gt; (TypeScript/Node, also ships a browser helper), &lt;code&gt;webauthn4j&lt;/code&gt; (Java), &lt;code&gt;Fido2NetLib&lt;/code&gt; (.NET), &lt;code&gt;go-webauthn/webauthn&lt;/code&gt; (Go, the actively maintained fork of the original Duo Labs library), &lt;code&gt;py_webauthn&lt;/code&gt; (Python), &lt;code&gt;webauthn-rs&lt;/code&gt; (Rust). All of them expose roughly the same two functions you need: “generate registration/authentication options” and “verify registration/authentication response.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Get the Relying Party ID right before anything else.&lt;/strong&gt; The &lt;code&gt;rpID&lt;/code&gt; is the domain the credential is bound to (no scheme, no port) — &lt;code&gt;example.com&lt;/code&gt;, not &lt;code&gt;https://example.com&lt;/code&gt; or &lt;code&gt;app.example.com&lt;/code&gt; if you also need it valid on &lt;code&gt;www.example.com&lt;/code&gt;. Get this wrong and passkeys registered in one environment silently fail to authenticate in another (a very common “works locally, breaks in staging” bug, because &lt;code&gt;localhost&lt;/code&gt; and your staging subdomain are different RP IDs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Require resident keys and conditional UI from day one&lt;/strong&gt; , not as a later enhancement — a passkey rollout without the autofill-picker UX gets a fraction of the adoption, because users never discover it is an option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Design account recovery before you ship, not after the first support ticket.&lt;/strong&gt; This is the part every “add passkeys” tutorial skips. At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let users register &lt;strong&gt;more than one&lt;/strong&gt; authenticator (phone + a backup security key, or two synced devices) — a single point of failure with an unrecoverable credential is worse than a password.&lt;/li&gt;
&lt;li&gt;Keep a fallback path (email magic link, backup codes generated at enrollment, or a human-verified support flow) for the case where every registered authenticator is lost.&lt;/li&gt;
&lt;li&gt;Decide explicitly whether you allow &lt;strong&gt;downgrading&lt;/strong&gt; to a password/OTP fallback per-account, and if so, gate it behind extra friction — it is the path an attacker will try first once passkeys make direct credential theft useless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Migrate gradually, not as a hard cutover.&lt;/strong&gt; Add passkeys as an &lt;em&gt;additional&lt;/em&gt; credential option alongside existing password/MFA, prompt existing users to enroll one after a successful login (when you already have a trusted session to authorize the enrollment), and only consider removing password login once enrollment is high and recovery paths are proven. If your session/token layer needs work in parallel, see &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/security/stop-storing-jwts-in-localstorage-cookie-auth-for-spas-2026" rel="noopener noreferrer"&gt;Stop Storing JWTs in localStorage&lt;/a&gt; — the two problems (how you authenticate, how you keep someone authenticated) are separate and often get conflated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Test the failure paths, not just the happy path&lt;/strong&gt; : a user who cancels the OS biometric prompt, a browser with no platform authenticator (older desktop Safari/Firefox configurations), a user switching from a lost Android phone to a new iPhone mid-session. Passkeys degrade differently than passwords do, and “no fallback UI for a cancelled ceremony” is the most common bug in first passkey implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Still Breaks in Practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-ecosystem migration is genuinely unfinished.&lt;/strong&gt; A user moving from Android to iPhone today re-enrolls passkeys per-site rather than carrying them over, unless they were using a cross-platform manager like 1Password or Bitwarden from the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise device loss is an operational problem, not just a cryptographic one.&lt;/strong&gt; Someone’s laptop with device-bound corporate passkeys is stolen — who revokes it, how fast, and does your IdP even expose per-credential revocation cleanly? Test this before you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every user has a passkey-capable device.&lt;/strong&gt; Older phones, shared/kiosk devices, and some enterprise-locked-down machines cannot register a platform authenticator; you need a real fallback, not a workaround bolted on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Passkey” as a support term is confusing precisely because it hides the sync-fabric question.&lt;/strong&gt; Support staff need to know to ask “which device and which account” was used to create it, because the recovery path is completely different for a synced iCloud Keychain passkey versus a device-bound YubiKey.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The cryptography is not the hard part — WebAuthn is a well-specified, narrow protocol, and the registration/login ceremonies above are genuinely all there is to the core exchange. The hard part is everything the protocol deliberately leaves to you: account recovery, fallback UX, and the fact that “passkey” quietly means four different sync fabrics with different failure modes depending on which vendor’s cloud a user happens to be in.&lt;/p&gt;

&lt;p&gt;If you are not already an identity provider, the pragmatic move is to let one absorb this problem for you — Okta, Auth0, Entra ID, or a self-hosted Keycloak/Authentik — and spend your own engineering time on account recovery and rollout sequencing instead of CBOR parsing. If you &lt;em&gt;are&lt;/em&gt; the relying party, the protocol rewards careful reading far more than most auth features do, and the payoff is real: there is no password database left to breach, and no login page left to phish in the way that has worked for thirty years.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/security/passkeys-vs-passwords-webauthn-how-it-works-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>passkeys</category>
      <category>webauthn</category>
      <category>fido2</category>
      <category>passwordlessauthenti</category>
    </item>
    <item>
      <title>Passkeys vs. Passwörter: Wie WebAuthn wirklich funktioniert, wer was anbietet und wie man es einführt</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Mon, 14 Sep 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/passkeys-vs-passworter-wie-webauthn-wirklich-funktioniert-wer-was-anbietet-und-wie-man-es-583l</link>
      <guid>https://dev.to/aleksei_aleinikov/passkeys-vs-passworter-wie-webauthn-wirklich-funktioniert-wer-was-anbietet-und-wie-man-es-583l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" alt="Passkeys vs. Passwörter: Wie WebAuthn wirklich funktioniert, wer was anbietet und wie man es einführt" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ein Passwort ist ein Geheimnis, das zwei Parteien gleichzeitig aufbewahren müssen: Sie selbst und jeder Server, in den Sie es je eingetippt haben. Das ist der eigentliche Konstruktionsfehler. Es spielt keine Rolle, wie lang oder zufällig die Zeichenfolge ist — wenn sie getippt werden kann, kann sie gephisht werden, und wenn sie noch irgendwo anders gespeichert ist, kann sie durch einen Breach abgegriffen und wiederverwendet werden.&lt;/p&gt;

&lt;p&gt;Ein Passkey ist kein stärkeres Geheimnis. Auf Serverseite ist er überhaupt kein Geheimnis. Er ist eine Hälfte eines Schlüsselpaars, und die Hälfte, die der Server bekommt, ist für einen Angreifer mathematisch wertlos.&lt;/p&gt;

&lt;p&gt;Diese Unterscheidung geht in den meisten “Aktiviere Passkeys”-Marketingtexten unter, was schade ist, denn das zugrunde liegende Protokoll ist wirklich elegant und lohnt sich, es auf Nachrichtenebene zu verstehen — besonders wenn Sie es implementieren müssen. Dieser Artikel behandelt, wie die beiden zentralen WebAuthn-Zeremonien tatsächlich funktionieren, was “synchronisiert” versus “geräte-gebunden” wirklich bedeutet, wer 2026 Passkeys anbietet und was diese jeweils vorhalten, wie man Unterstützung implementiert, ohne die Kontowiederherstellung zu zerstören, und eine ehrliche Einschätzung, wo es noch hakt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsa47hqm4edofyi1ydv3.webp" alt="Passwörter sind ein Geheimnis, das man ständig weitergibt. Passkeys ersetzen das durch ein Schlüsselpaar, das nur das eigene Gerät besitzt — Vergleich der Risiken von Passwörtern mit den Eigenschaften von Passkeys" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Was ein Passkey wirklich ist
&lt;/h2&gt;

&lt;p&gt;Entfernt man das Produkt-Framing, ist ein Passkey ein WebAuthn-Public-Key-Credential, und WebAuthn ist ein W3C-Standard, der zusammen mit dem CTAP2-Protokoll der FIDO Alliance &lt;strong&gt;FIDO2&lt;/strong&gt; bildet. Drei Parteien sind an jeder Zeremonie beteiligt, und es lohnt sich, sie präzise zu benennen, weil die Anbieter-Dokumentation das selten tut:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Die Relying Party (RP)&lt;/strong&gt; — die Website oder App, die Sie authentifizieren möchte. Das ist Ihr Server. Er sieht, speichert oder überträgt zu keinem Zeitpunkt einen privaten Schlüssel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Der Client&lt;/strong&gt; — Browser oder Betriebssystem, der zwischen dem JavaScript der RP (&lt;code&gt;navigator.credentials&lt;/code&gt;) und dem Authenticator vermittelt und — das ist der wichtige Teil — den Origin unabhängig prüft, bevor irgendetwas signiert wird.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Der Authenticator&lt;/strong&gt; — das Ding, das den privaten Schlüssel tatsächlich hält: die Secure Enclave eines Smartphones, das TPM eines Laptops, oder ein Roaming-Hardware-Key (YubiKey, Titan). Er spricht nie direkt mit dem Netzwerk, sondern nur über CTAP2 mit dem Client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ein Passwort-Login hat zwei Parteien und ein gemeinsames Geheimnis. Ein Passkey-Login hat drei Parteien und null gemeinsame Geheimnisse — der öffentliche Schlüssel, der auf Ihrem Server liegt, darf per Definition geleakt werden. Das ist der ganze Punkt, und alles Weitere in diesem Artikel beschreibt, wie diese Eigenschaft erreicht wird und wo sie doch wieder mit einem “na ja, es ist kompliziert” zurückkommt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wie die Registrierung funktioniert
&lt;/h2&gt;

&lt;p&gt;Einem Konto “einen Passkey hinzuzufügen” ist eine WebAuthn- &lt;strong&gt;Registrierungszeremonie&lt;/strong&gt; (&lt;code&gt;navigator.credentials.create()&lt;/code&gt;). Sechs Nachrichten, drei Parteien, und der private Schlüssel berührt genau eine davon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva2yzu72jch8kyj22t97.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva2yzu72jch8kyj22t97.webp" alt="Diagramm der WebAuthn-Registrierungszeremonie: Die Relying Party sendet eine Challenge, der Browser ruft navigator.credentials.create auf, der Authenticator erzeugt ein Schlüsselpaar auf dem Gerät und gibt den öffentlichen Schlüssel zurück, der Server speichert nur den öffentlichen Schlüssel" width="800" height="467"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Der private Schlüssel entsteht in Schritt 3 und taucht in keiner der übrigen fünf Nachrichten auf.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ein paar Details, die relevant werden, sobald man das tatsächlich implementiert:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Die Challenge ist Einmalgebrauch und wird serverseitig erzeugt.&lt;/strong&gt; Sie dient rein der Replay-Prävention — die Signatur, die der Authenticator am Ende erzeugt, ist nur für genau diese Challenge gültig, sodass eine abgefangene Registrierungsantwort nicht gegen eine andere Session wiederverwendet werden kann.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attestation ist optional und meist nicht erzwingenswert.&lt;/strong&gt; Der Authenticator kann kryptografisch belegen, welches Modell er ist (&lt;code&gt;attestation: "direct"&lt;/code&gt;), aber das leakt Geräte-Fingerprinting-Informationen, erfordert die Pflege eines Metadata-Trust-Stores, und die meisten Consumer-Flows nutzen &lt;code&gt;attestation: "none"&lt;/code&gt;. Direkte Attestation nur in regulierten Enterprise-Kontexten einsetzen, in denen eine bestimmte Hardware-Allowlist durchgesetzt werden muss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entdeckbare (Resident) Credentials sind das, was “Passkey” vom alten WebAuthn-2FA unterscheidet.&lt;/strong&gt; &lt;code&gt;residentKey: "required"&lt;/code&gt; weist den Authenticator an, das Credential so zu speichern, dass es &lt;em&gt;ohne&lt;/em&gt; vorherige Übermittlung eines Benutzernamens gefunden werden kann — genau das treibt die “Mit Passkey anmelden”-Kontoauswahl an, statt “Benutzernamen eingeben, dann Security-Key benutzen”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential-ID und öffentlicher Schlüssel sind das, was gespeichert wird — sonst nichts.&lt;/strong&gt; Ein minimales Schema ist &lt;code&gt;(user_id, credential_id, public_key_cose, sign_count, transports, created_at)&lt;/code&gt;. &lt;code&gt;sign_count&lt;/code&gt; ist ein Legacy-Anti-Klon-Zähler aus Einzelgeräte-Authenticatoren; die meisten synchronisierten Passkeys melden ihn konstant als &lt;code&gt;0&lt;/code&gt; — als Hinweis behandeln, nicht als Sicherheitsgrenze.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wie der Login funktioniert
&lt;/h2&gt;

&lt;p&gt;Das Anmelden ist die &lt;strong&gt;Authentifizierungszeremonie&lt;/strong&gt; (&lt;code&gt;navigator.credentials.get()&lt;/code&gt;) und das Spiegelbild der Registrierung — eine Challenge geht raus, eine Signatur kommt zurück, nur dass der Server diesmal etwas hat, gegen das er die Signatur prüfen kann.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqw5rm5o4raqpf6ac95m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqw5rm5o4raqpf6ac95m.webp" alt="Diagramm, das denselben Passkey-Login-Versuch auf der echten Seite yourapp.com mit einer Phishing-Fake-Domain vergleicht: Auf dem echten Origin signiert der Authenticator die Challenge und eine Session entsteht, auf der Phishing-Domain verweigert der Authenticator die Signatur und es gibt nichts zu stehlen" width="800" height="467"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Gleicher Nutzer, gleicher Tap, zwei verschiedene Origins — der Browser entscheidet das Ergebnis, bevor überhaupt eine Signatur entsteht.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Das Detail, das die eigentliche Anti-Phishing-Eigenschaft liefert, überliest man leicht: &lt;strong&gt;der Client — nicht der Nutzer — prüft, ob der Origin, der die Assertion anfordert, mit dem Origin übereinstimmt, gegen den das Credential registriert wurde.&lt;/strong&gt; Ein pixelgenauer Klon Ihrer Login-Seite auf einer Fake-Domain kann einen identischen &lt;code&gt;navigator.credentials.get()&lt;/code&gt;-Aufruf rendern, und der Browser wird schlicht keine gültige Signatur dafür erzeugen. Es gibt kein “Passwort in die falsche Box eingetippt”-Fehlerbild, weil es keine Box gibt.&lt;/p&gt;

&lt;p&gt;Zwei Implementierungshinweise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conditional UI (Autofill) ist das, was sich wie ein Passwortfeld anfühlt.&lt;/strong&gt; Ruft man &lt;code&gt;get()&lt;/code&gt; beim Laden der Seite mit &lt;code&gt;mediation: "conditional"&lt;/code&gt; auf, zeigt der Browser gespeicherte Passkeys im normalen Autofill-Dropdown des Benutzernamens an — das Login-Formular &lt;em&gt;sieht&lt;/em&gt; also weiterhin wie ein einfaches Eingabefeld aus, bietet aber zusätzlich eine Ein-Klick-Passkey-Option. Das auszulassen ist der häufigste Grund, warum “wir haben Passkeys eingeführt”-Rollouts eine geringe Adoption erzielen: Nutzer sehen die Option nie, weil sie hinter einem separaten Button versteckt ist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Verification (&lt;code&gt;userVerification: "preferred"&lt;/code&gt; vs. &lt;code&gt;"required"&lt;/code&gt;) entscheidet, ob eine reine “Gerät ist anwesend”-Prüfung ausreicht&lt;/strong&gt; oder ob die lokale biometrische/PIN-Entsperrung zwingend ist. Für alles jenseits eines trivialen Kontos sollte sie verpflichtend sein — sonst wird ein entsperrtes, unbeaufsichtigtes Gerät zu einem vollständigen Authentifizierungs-Bypass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wer den privaten Schlüssel tatsächlich hält
&lt;/h2&gt;

&lt;p&gt;Hier wird das “nimm einfach Passkeys”-Versprechen weniger einfach, und es lohnt sich, präzise zu sein, weil es das Design der Kontowiederherstellung verändert.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7c4xe758igc3lpzp3iy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7c4xe758igc3lpzp3iy.webp" alt="Diagramm der Passkey-Sync-Ökosysteme 2026: Apple iCloud Keychain, Google Passwortmanager, Microsoft Windows Hello und Entra ID sowie plattformübergreifende Passwortmanager wie 1Password und Bitwarden, verbunden nur über FIDO2-CTAP2-Hybrid-Transport per QR-Code" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Vier Sync-Ökosysteme, ein ökosystemübergreifender Fallback — und das ist eine Transportbrücke, kein Credential-Migrationspfad.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synchronisierte Passkeys&lt;/strong&gt; leben in der Ende-zu-Ende-verschlüsselten Cloud eines Plattformanbieters und folgen dem Konto des Nutzers, nicht seinem Gerät:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple&lt;/strong&gt; synchronisiert über iCloud Keychain über macOS, iOS, iPadOS und visionOS hinweg, abgesichert durch die Apple ID und (zum Anzeigen) den Geräte-Passcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; synchronisiert über den Google Passwortmanager über Android, ChromeOS und Chrome auf jedem Betriebssystem, gebunden an das Google-Konto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft&lt;/strong&gt; verankert Plattform-Passkeys an Windows Hello und das Microsoft-Konto; auf der Workforce-Seite fügt Entra ID Richtlinienkontrolle hinzu, welche Authenticatoren akzeptabel sind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1Password, Bitwarden, Dashlane&lt;/strong&gt; und andere Passwortmanager synchronisieren Passkeys über den eigenen Tresor, plattformübergreifend per Design — genau darum geht es bei einem Passwortmanager: nicht an einen OS-Anbieter gebunden zu sein.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Geräte-gebundene Passkeys&lt;/strong&gt; sind das andere Modell: Der private Schlüssel wird in genau einem Stück Hardware erzeugt und niemals exportiert, gesichert oder synchronisiert — per Design. Das ist der Standard (und oft die &lt;em&gt;einzige&lt;/em&gt; Option) bei FIDO2-Hardware-Security-Keys, und manche Enterprise-MDM-Richtlinien erzwingen das sogar für Plattform-Authenticatoren — Wiederherstellbarkeit gegen eine deutlich schmalere Angriffsfläche eingetauscht: Der Schlüssel kann nur durch physische Kompromittierung genau dieses einen Geräts exfiltriert werden.&lt;/p&gt;

&lt;p&gt;Was &lt;strong&gt;noch nicht&lt;/strong&gt; sauber funktioniert: Einen synchronisierten Passkey von einem Anbieter-Ökosystem in ein anderes zu übertragen. Ein in iCloud Keychain erzeugter Passkey taucht nicht im Google Passwortmanager auf. Die praktische Brücke über Ökosysteme hinweg ist der &lt;strong&gt;FIDO2-CTAP2-Hybrid-Transport&lt;/strong&gt; — einen QR-Code auf einem Laptop scannen, auf dem Smartphone per Bluetooth bestätigen, und das Smartphone fungiert für diese Browser-Session als einmaliger Roaming-Authenticator. Das ist ein echt nützlicher Fallback für “ich bin am Rechner eines Freundes” oder eine Plattformlücke, aber der private Schlüssel verlässt das Smartphone nach wie vor nie, und es ist kein Migrationswerkzeug. Das Credential Exchange Protocol (CXP) der FIDO Alliance ist die Standardisierungsinitiative für einen sauberen anbieterübergreifenden Export/Import; Stand 2026 ist das noch früh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wer 2026 was anbietet
&lt;/h2&gt;

&lt;p&gt;Für die meisten Teams ist die eigentliche Entscheidung nicht “sollen wir rohes WebAuthn implementieren”, sondern “auf welcher Ebene implementieren wir es”. Hier die Landschaft, grob geordnet von der Consumer-Plattform bis zur Identitätsinfrastruktur:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anbieter&lt;/th&gt;
&lt;th&gt;Was es ist&lt;/th&gt;
&lt;th&gt;Rolle bei Passkeys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apple, Google, Microsoft&lt;/td&gt;
&lt;td&gt;Betriebssystem-/Browser-Plattformen&lt;/td&gt;
&lt;td&gt;Stellen Authenticator + Sync-Ökosystem (iCloud Keychain, Google Passwortmanager, Windows Hello), die Endnutzer bereits auf ihrem Gerät haben&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1Password, Bitwarden, Dashlane&lt;/td&gt;
&lt;td&gt;Passwortmanager&lt;/td&gt;
&lt;td&gt;Speichern und synchronisieren Passkeys über jedes Betriebssystem/jeden Browser als Tresor-Eintrag, unabhängig von einem einzelnen Plattformanbieter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Okta, Auth0, Microsoft Entra ID, Ping Identity&lt;/td&gt;
&lt;td&gt;Enterprise-IdPs&lt;/td&gt;
&lt;td&gt;Ermöglichen Passkey-Login für Workforce oder Kunden-App, ohne selbst WebAuthn-Code anzufassen — der IdP ist die Relying Party&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak, Authentik&lt;/td&gt;
&lt;td&gt;Selbst gehostete IdPs&lt;/td&gt;
&lt;td&gt;Dieselbe Idee, Open Source — siehe &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/security/ist-keycloak-kostenlos-keycloak-vs-authentik-2026" rel="noopener noreferrer"&gt;Keycloak vs. authentik&lt;/a&gt;, falls Sie zwischen beiden wählen; beide unterstützen WebAuthn als Self-Service-Credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Cognito, Clerk&lt;/td&gt;
&lt;td&gt;Verwaltete App-Auth-Plattformen&lt;/td&gt;
&lt;td&gt;Bieten WebAuthn-/Passkey-Login als eingebaute Konfigurationsoption — kein eigener Zeremonie-Code nötig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleWebAuthn, &lt;code&gt;webauthn4j&lt;/code&gt;, &lt;code&gt;go-webauthn/webauthn&lt;/code&gt;, py_webauthn&lt;/td&gt;
&lt;td&gt;Open-Source-Relying-Party-Libraries&lt;/td&gt;
&lt;td&gt;Das, worauf man zurückgreift, wenn man die Identitätsebene &lt;em&gt;selbst&lt;/em&gt; baut — die Zeremonie parsen/verifizieren, ohne CBOR/COSE selbst zu implementieren&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yubico, Google (Titan), SoloKeys&lt;/td&gt;
&lt;td&gt;Hardware-Security-Keys&lt;/td&gt;
&lt;td&gt;Geräte-gebundene Roaming-Authenticatoren für Workforce-/High-Assurance-Konten, unabhängig von jedem OS-Sync-Ökosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Die ehrliche Einschätzung: &lt;strong&gt;Wenn Ihr Produkt bereits hinter Okta, Auth0, Entra ID oder einer selbst gehosteten Keycloak-/Authentik-Instanz sitzt, ist das Aktivieren von Passkeys eine Konfigurationsänderung, kein Engineering-Projekt.&lt;/strong&gt; Der IdP ist bereits die Relying Party; Sie aktivieren nur einen Credential-Typ, den er bereits kennt. Das im Folgenden beschriebene Engineering-Projekt gilt für den Fall, dass &lt;em&gt;Sie selbst&lt;/em&gt; die Relying Party sind — Sie betreiben Ihren eigenen Auth-Stack und wollen Passkeys als vollwertiges Credential hinzufügen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wie man es tatsächlich implementiert
&lt;/h2&gt;

&lt;p&gt;Angenommen, Sie sind die Relying Party. So sieht die Arbeit aus:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Eine Server-Library wählen — CBOR/COSE nicht von Hand parsen.&lt;/strong&gt; Das WebAuthn-Wire-Format (Attestation-Objekte, COSE-Public-Keys, CBOR-kodierte Authenticator-Daten) ist genau die Art von Sache, bei der es leicht passiert, sie subtil falsch zu machen — auf eine Weise, die erst später als Sicherheitslücke auffällt. Eine gepflegte Library für den eigenen Stack verwenden: &lt;a href="https://simplewebauthn.dev/" rel="noopener noreferrer"&gt;SimpleWebAuthn&lt;/a&gt; (TypeScript/Node, liefert auch einen Browser-Helper), &lt;code&gt;webauthn4j&lt;/code&gt; (Java), &lt;code&gt;Fido2NetLib&lt;/code&gt; (.NET), &lt;code&gt;go-webauthn/webauthn&lt;/code&gt; (Go, der aktiv gepflegte Fork der ursprünglichen Duo-Labs-Library), &lt;code&gt;py_webauthn&lt;/code&gt; (Python), &lt;code&gt;webauthn-rs&lt;/code&gt; (Rust). Alle stellen ungefähr dieselben zwei Funktionen bereit: “Registrierungs-/Authentifizierungs-Optionen erzeugen” und “Registrierungs-/Authentifizierungs-Antwort verifizieren”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Die Relying Party ID von Anfang an richtig setzen.&lt;/strong&gt; Die &lt;code&gt;rpID&lt;/code&gt; ist die Domain, an die das Credential gebunden wird (kein Schema, kein Port) — &lt;code&gt;example.com&lt;/code&gt;, nicht &lt;code&gt;https://example.com&lt;/code&gt;, und nicht &lt;code&gt;app.example.com&lt;/code&gt;, falls es auch auf &lt;code&gt;www.example.com&lt;/code&gt; gültig sein muss. Hier einen Fehler zu machen führt dazu, dass in einer Umgebung registrierte Passkeys in einer anderen still und leise nicht authentifizieren (ein sehr häufiger “funktioniert lokal, bricht in Staging”-Bug, weil &lt;code&gt;localhost&lt;/code&gt; und die Staging-Subdomain unterschiedliche RP-IDs sind).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Resident Keys und Conditional UI von Tag eins an erzwingen&lt;/strong&gt; , nicht als spätere Erweiterung — ein Passkey-Rollout ohne die Autofill-Kontoauswahl-UX erreicht nur einen Bruchteil der Adoption, weil Nutzer nie entdecken, dass es die Option gibt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Kontowiederherstellung entwerfen, bevor man live geht, nicht nach dem ersten Support-Ticket.&lt;/strong&gt; Das ist der Teil, den jedes “Passkeys hinzufügen”-Tutorial auslässt. Mindestens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nutzern erlauben, &lt;strong&gt;mehr als einen&lt;/strong&gt; Authenticator zu registrieren (Smartphone plus ein Backup-Security-Key, oder zwei synchronisierte Geräte) — ein Single Point of Failure mit einem unwiederbringlichen Credential ist schlimmer als ein Passwort.&lt;/li&gt;
&lt;li&gt;Einen Fallback-Pfad vorhalten (E-Mail-Magic-Link, bei der Registrierung generierte Backup-Codes, oder ein menschlich verifizierter Support-Flow) für den Fall, dass jeder registrierte Authenticator verloren ist.&lt;/li&gt;
&lt;li&gt;Explizit entscheiden, ob ein &lt;strong&gt;Downgrade&lt;/strong&gt; auf Passwort/OTP pro Konto erlaubt ist, und falls ja, es hinter zusätzlicher Friktion absichern — das ist der Pfad, den ein Angreifer als Erstes probieren wird, sobald Passkeys direkten Credential-Diebstahl nutzlos machen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Schrittweise migrieren, nicht als harten Cutover.&lt;/strong&gt; Passkeys als &lt;em&gt;zusätzliche&lt;/em&gt; Credential-Option neben bestehendem Passwort/MFA anbieten, bestehende Nutzer nach einem erfolgreichen Login zur Registrierung auffordern (wenn bereits eine vertrauenswürdige Session existiert, um die Registrierung zu autorisieren), und Passwort-Login erst dann entfernen, wenn die Registrierungsrate hoch und die Wiederherstellungspfade bewährt sind. Falls parallel an der Session-/Token-Ebene gearbeitet werden muss, siehe &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/security/jwt-nicht-in-localstorage-cookie-auth-fuer-spas-2026" rel="noopener noreferrer"&gt;JWTs nicht in localStorage&lt;/a&gt; — die beiden Probleme (wie man authentifiziert, wie man jemanden authentifiziert hält) sind getrennt und werden häufig vermischt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Die Fehlerpfade testen, nicht nur den Happy Path&lt;/strong&gt; : ein Nutzer, der den biometrischen OS-Prompt abbricht, ein Browser ohne Plattform-Authenticator (ältere Desktop-Safari-/Firefox-Konfigurationen), ein Nutzer, der mitten in der Session von einem verlorenen Android-Smartphone auf ein neues iPhone wechselt. Passkeys degradieren anders als Passwörter, und “keine Fallback-UI für eine abgebrochene Zeremonie” ist der häufigste Bug in ersten Passkey-Implementierungen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was in der Praxis noch hakt
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ökosystemübergreifende Migration ist wirklich noch unfertig.&lt;/strong&gt; Ein Nutzer, der heute von Android auf iPhone wechselt, registriert Passkeys pro Website neu, außer er nutzte von Anfang an einen plattformübergreifenden Manager wie 1Password oder Bitwarden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geräteverlust im Unternehmen ist ein operatives Problem, nicht nur ein kryptografisches.&lt;/strong&gt; Jemandes Laptop mit geräte-gebundenen Firmen-Passkeys wird gestohlen — wer widerruft das, wie schnell, und legt der eigene IdP überhaupt einen sauberen Weg für Credential-Widerruf frei? Das testen, bevor man es braucht.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nicht jeder Nutzer hat ein passkey-fähiges Gerät.&lt;/strong&gt; Ältere Smartphones, geteilte/Kiosk-Geräte und manche unternehmenseigene, gesperrte Rechner können keinen Plattform-Authenticator registrieren; ein echter Fallback wird gebraucht, kein angeflanschter Workaround.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Passkey” als Support-Begriff ist gerade deshalb verwirrend, weil er die Sync-Ökosystem-Frage verdeckt.&lt;/strong&gt; Support-Mitarbeiter müssen wissen, dass sie fragen müssen, “mit welchem Gerät und welchem Konto” er erstellt wurde, weil der Wiederherstellungspfad für einen synchronisierten iCloud-Keychain-Passkey völlig anders ist als für einen geräte-gebundenen YubiKey.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fazit
&lt;/h2&gt;

&lt;p&gt;Die Kryptografie ist nicht der schwierige Teil — WebAuthn ist ein gut spezifiziertes, eng gefasstes Protokoll, und die oben beschriebenen Registrierungs-/Login-Zeremonien sind wirklich alles, was zum Kernaustausch gehört. Der schwierige Teil ist alles, was das Protokoll bewusst offenlässt: Kontowiederherstellung, Fallback-UX, und die Tatsache, dass “Passkey” leise für vier verschiedene Sync-Ökosysteme mit unterschiedlichen Fehlermodi steht, je nachdem, in welcher Anbieter-Cloud ein Nutzer gerade zufällig ist.&lt;/p&gt;

&lt;p&gt;Wer noch kein Identity Provider ist, für den ist der pragmatische Schritt, dieses Problem von einem übernehmen zu lassen — Okta, Auth0, Entra ID, oder ein selbst gehostetes Keycloak/Authentik — und die eigene Engineering-Zeit in Kontowiederherstellung und Rollout-Reihenfolge zu stecken statt in CBOR-Parsing. Wer selbst die Relying Party ist, für den belohnt das Protokoll sorgfältiges Lesen weit mehr als die meisten Auth-Features, und die Auszahlung ist real: keine Passwort-Datenbank mehr, die man breachen kann, und keine Login-Seite mehr, die sich phishen lässt auf die Art, wie es dreißig Jahre lang funktioniert hat.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ursprünglich veröffentlicht auf &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/security/passkeys-vs-passwoerter-wie-webauthn-funktioniert-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>passkeys</category>
      <category>webauthn</category>
      <category>fido2</category>
      <category>passwortloseauthenti</category>
    </item>
    <item>
      <title>Scalability: The Bottleneck Is Almost Never the CPU</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/scalability-the-bottleneck-is-almost-never-the-cpu-4k6f</link>
      <guid>https://dev.to/aleksei_aleinikov/scalability-the-bottleneck-is-almost-never-the-cpu-4k6f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" alt="Scalability: The Bottleneck Is Almost Never the CPU" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your dashboard shows 28% CPU. Your p99 latency is 4 seconds. Someone suggests adding more pods.&lt;/p&gt;

&lt;p&gt;This is the most common wrong turn in scaling work, and it comes from a reasonable assumption: that a slow system must be a busy system. It usually is not. It is a system where requests are queuing for something that is not CPU, and adding instances often makes it worse.&lt;/p&gt;

&lt;p&gt;Here is what is actually limiting you, and the arithmetic to find it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" alt="Scalability bottlenecks: why CPU is idle while requests queue elsewhere." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One formula that sizes almost everything
&lt;/h2&gt;

&lt;p&gt;Little’s Law is the most useful thing in capacity planning, and it fits on one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concurrency = arrival rate x latency

L = λ × W
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;If 500 requests arrive per second and each spends 40 ms in the database, then at any instant you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 req/s × 0.04 s = 20 concurrent database connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Twenty. Not two hundred.&lt;/p&gt;

&lt;p&gt;Now run it backwards, which is where it earns its keep. You have a pool of &lt;strong&gt;20 connections&lt;/strong&gt; and each query takes &lt;strong&gt;50 ms&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;20 / 0.05 s = 400 requests per second
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;That is your ceiling.&lt;/strong&gt; Not a soft limit — a hard one. You can run forty pods; the pool still admits 400 requests per second and everything else waits. CPU stays low because the CPU is not doing anything. It is waiting.&lt;/p&gt;

&lt;p&gt;Every queue in your system obeys this. Thread pools, connection pools, worker counts, in-flight request limits. If you know two of the three terms, you know the third.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 80% utilisation is not 80% fine
&lt;/h2&gt;

&lt;p&gt;Here is the part that explains why capacity planning feels counter-intuitive.&lt;/p&gt;

&lt;p&gt;For a simple queue, response time relates to utilisation like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response time = service time / (1 - utilisation)

R = S / (1 - ρ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;That denominator is the whole story. As utilisation approaches 1, response time does not rise gently — it goes to infinity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Utilisation&lt;/th&gt;
&lt;th&gt;Response time vs. an idle system&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Going from 50% to 80% utilisation does not cost you 30% of your latency budget. It costs you &lt;strong&gt;150%&lt;/strong&gt; — response time goes from twice service time to five times it.&lt;/p&gt;

&lt;p&gt;This is why experienced teams keep what looks like wasteful headroom, and why a system that was fine yesterday at 70% falls over today at 85% after a modest traffic increase. Nothing changed except your position on a curve that turns vertical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest caveat:&lt;/strong&gt; this exact formula assumes a single server with Poisson arrivals and exponentially distributed service times — an M/M/1 queue. Real systems have multiple servers and burstier traffic, so your constants will differ. But the &lt;em&gt;shape&lt;/em&gt; is universal: queueing delay is hyperbolic in utilisation, not linear. Every queue you own has a knee, and it is closer to the left than intuition suggests.&lt;/p&gt;

&lt;p&gt;Practical consequence: &lt;strong&gt;stop setting autoscaling targets at 80% CPU&lt;/strong&gt;. By the time you are there you are already deep into the curve, and the new instance takes time to become useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceilings, in the order they usually bite
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Database connections
&lt;/h3&gt;

&lt;p&gt;This is the most common real limit, and it has a number you can look up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL defaults to &lt;code&gt;max_connections = 100&lt;/code&gt;&lt;/strong&gt; , of which &lt;strong&gt;3 are reserved for superusers&lt;/strong&gt; by default. So roughly 97 are actually available to your application.&lt;/p&gt;

&lt;p&gt;Now count what you are asking for. Twelve pods with a pool of 10 each is 120 connections against a limit of 97. The twelfth pod gets connection errors, and the error looks like a database problem rather than an arithmetic one.&lt;/p&gt;

&lt;p&gt;Two things make this sharper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL &lt;strong&gt;forks a process per connection&lt;/strong&gt; , so connections cost real memory. The docs are explicit that raising &lt;code&gt;max_connections&lt;/code&gt; raises shared memory allocation with it. This is why a pooler like PgBouncer exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bigger pool is not more throughput.&lt;/strong&gt; Past the point where the database can genuinely execute queries in parallel, extra connections add lock contention and context switching. Throughput flattens, then falls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MySQL uses a thread per connection and is cheaper here, which is one of the &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/data/postgres-vs-mysql-2026" rel="noopener noreferrer"&gt;structural differences between Postgres and MySQL&lt;/a&gt; worth knowing before you pick.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. File descriptors
&lt;/h3&gt;

&lt;p&gt;Every socket, every open file, every connection is a file descriptor. The per-process limit is often &lt;strong&gt;1024&lt;/strong&gt; by default on the soft limit, which sounds generous until you notice it covers inbound connections, outbound connections, log files and library internals simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="c"&gt;# soft limit for this shell&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/limits &lt;span class="c"&gt;# what the running process actually has&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/fd | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="c"&gt;# how many it is using right now&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;The failure mode is memorable: &lt;code&gt;EMFILE: too many open files&lt;/code&gt;, usually under exactly the load where you least want to be reading a stack trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ephemeral ports and TIME_WAIT
&lt;/h3&gt;

&lt;p&gt;This one surprises people because nothing in the application mentions it.&lt;/p&gt;

&lt;p&gt;When your service opens an outbound connection, the kernel picks a source port from the ephemeral range. On Linux:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_local_port_range

&lt;span class="c"&gt;# typically: 32768 60999&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;That is about &lt;strong&gt;28,000 ports&lt;/strong&gt; per unique destination address and port combination.&lt;/p&gt;

&lt;p&gt;Then the closing side holds the socket in &lt;strong&gt;TIME_WAIT for 60 seconds&lt;/strong&gt; — and on Linux that duration is compiled into the kernel, not a sysctl you can turn down.&lt;/p&gt;

&lt;p&gt;Do the arithmetic. Open 500 short-lived connections per second to the same downstream service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 conn/s × 60 s = 30,000 sockets in TIME_WAIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;You have run out. New connections fail, and the error will be a connect timeout that looks like the downstream service is down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is not raising the range.&lt;/strong&gt; It is not opening the connections: HTTP keep-alive, connection pooling, HTTP/2 multiplexing. Reuse beats provisioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Connection tracking
&lt;/h3&gt;

&lt;p&gt;If your traffic passes through a NAT gateway, a firewall or a Kubernetes node using iptables, the kernel keeps a &lt;strong&gt;conntrack&lt;/strong&gt; entry for every flow. That table is finite, and when it fills, packets are dropped silently — no error, no log line in your application, just latency and retries.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
sysctl net.netfilter.nf_conntrack_max

sysctl net.netfilter.nf_conntrack_count

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

&lt;/div&gt;





&lt;p&gt;The gap between those two numbers is a metric worth alerting on, and almost nobody exports it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Threads and event loops
&lt;/h3&gt;

&lt;p&gt;A thread-per-request server is a pool by another name, and Little’s Law applies unchanged. An event-loop server has no such limit — which is its own hazard, because it will happily accept ten thousand concurrent requests and serve all of them slowly instead of refusing some quickly.&lt;/p&gt;

&lt;p&gt;One blocking call on an event loop stalls every request on that loop. The CPU graph will look calm throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Horizontal scaling and the shared denominator
&lt;/h2&gt;

&lt;p&gt;Adding instances multiplies capacity for the &lt;strong&gt;independent&lt;/strong&gt; part of a request and does nothing for the shared part.&lt;/p&gt;

&lt;p&gt;Amdahl’s Law puts a ceiling on the speedup available from parallelising anything with a serial fraction. The &lt;strong&gt;Universal Scalability Law&lt;/strong&gt; adds the part people miss, and it is worth writing out because the second term is the one that bites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C(N) = N / (1 + σ(N-1) + κN(N-1))

  N number of workers

  σ contention — the serial fraction, queueing for a shared resource

  κ coherency — the cost of keeping workers consistent with each other

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

&lt;/div&gt;





&lt;p&gt;With κ = 0 this reduces to Amdahl’s Law: throughput flattens but never falls. With &lt;strong&gt;κ &amp;gt; 0&lt;/strong&gt; something worse happens — there is a maximum, and past it throughput &lt;strong&gt;decreases&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;peak at N* = sqrt((1 - σ) / κ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;The coherency term is why adding capacity can make a system slower. It covers cache-line bouncing between cores, lock handoffs, cluster gossip, distributed consensus, cross-region replication — anything whose cost grows with the &lt;em&gt;square&lt;/em&gt; of the number of participants because each one must agree with every other.&lt;/p&gt;

&lt;p&gt;Concretely, if every new pod opens more connections to the same primary database, you are not scaling. You are converting a connection limit into a queue and then into a timeout. If every new node must gossip with every other node, you have bought yourself a κ term.&lt;/p&gt;

&lt;p&gt;Ask this before adding instances:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What does a request touch that is &lt;strong&gt;shared&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;What is the current concurrency limit on that shared thing?&lt;/li&gt;
&lt;li&gt;Does another instance &lt;strong&gt;raise&lt;/strong&gt; that limit, or &lt;strong&gt;compete&lt;/strong&gt; for it?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answer to 3 is “compete”, scaling out makes it worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load balancing: what the diagrams leave out
&lt;/h2&gt;

&lt;p&gt;The box labelled “LB” in your architecture diagram hides three decisions that determine whether scaling works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks that lie.&lt;/strong&gt; A check that returns 200 whenever the process is alive will keep routing traffic into an instance whose database pool is exhausted and whose queue is thirty seconds deep. A useful check verifies the dependencies a request actually needs — and distinguishes &lt;em&gt;liveness&lt;/em&gt; (restart me) from &lt;em&gt;readiness&lt;/em&gt; (stop sending me traffic). Conflating them causes restart loops under load, exactly when you need stability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L4 versus L7 under HTTP/2.&lt;/strong&gt; L4 balances &lt;strong&gt;connections&lt;/strong&gt;. With HTTP/2 or gRPC, one long-lived connection carries many multiplexed requests — so connection-level balancing can leave one backend doing most of the work while the others idle. The graph shows even connection counts and wildly uneven CPU. For HTTP/2 and gRPC you generally want &lt;strong&gt;request-level&lt;/strong&gt; balancing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky sessions.&lt;/strong&gt; Pinning a user to an instance defeats balancing on purpose. It also means a restart drops those users’ state, and a hot user becomes a hot instance you cannot spread.&lt;/p&gt;

&lt;p&gt;Then there is the retry trap: when a system is already saturated, automatic retries multiply the load that saturated it. Retries need &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/architecture/rate-limiting-in-practice-algorithms-headers-2026" rel="noopener noreferrer"&gt;rate limiting and a budget&lt;/a&gt;, plus jitter, or they turn a brown-out into an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queues absorb bursts, not overload
&lt;/h2&gt;

&lt;p&gt;A queue in front of a slow consumer buys you time across a &lt;strong&gt;burst&lt;/strong&gt;. It does nothing about a sustained arrival rate above your service rate — it just moves where the failure appears.&lt;/p&gt;

&lt;p&gt;If arrivals exceed service capacity indefinitely, the queue grows without bound, and latency grows with it. Callers time out. They retry. Arrivals increase. This is a feedback loop with the sign pointing the wrong way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backpressure&lt;/strong&gt; is the answer, and it means being willing to say no:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded queues.&lt;/strong&gt; An unbounded queue is an out-of-memory error with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject fast.&lt;/strong&gt; A 429 with &lt;code&gt;Retry-After&lt;/code&gt; in 5 ms is far kinder than a timeout at 30 seconds — to the caller and to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shed by value.&lt;/strong&gt; Under pressure, drop the batch export before you drop the checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap in-flight work.&lt;/strong&gt; Concurrency limits per dependency stop one slow downstream from consuming every worker you have.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The instinct is that rejecting requests is failure. The alternative is accepting requests you cannot serve, which is also failure — just later, and for everyone at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What multi-region does and does not give you
&lt;/h2&gt;

&lt;p&gt;Multi-region buys you &lt;strong&gt;availability&lt;/strong&gt; when a region fails and &lt;strong&gt;lower latency&lt;/strong&gt; for users near a region.&lt;/p&gt;

&lt;p&gt;It does not buy you throughput on anything that must stay consistent. A globally consistent write still costs at least one cross-region round trip, and the speed of light is not a configuration parameter — roughly 5 ms per 1,000 km in fibre before any equipment touches the packet. Frankfurt to Virginia is a physical floor of tens of milliseconds, per round trip.&lt;/p&gt;

&lt;p&gt;If your write path crosses regions, you have not scaled writes. You have added latency and a partition mode to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the actual bottleneck
&lt;/h2&gt;

&lt;p&gt;Brendan Gregg’s &lt;strong&gt;USE method&lt;/strong&gt; is the fastest route. For every resource, measure three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Utilisation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;% of time the resource is busy&lt;/td&gt;
&lt;td&gt;Misleads on its own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Saturation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much work is &lt;strong&gt;queued&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Usually the real signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Errors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rejections, timeouts, drops&lt;/td&gt;
&lt;td&gt;Tells you what already broke&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most teams export utilisation and stop. But a pool at 100% utilisation with an empty queue is perfectly healthy, while a pool at 60% with a queue thirty deep is your outage. &lt;strong&gt;Saturation is the metric that names the bottleneck, and it is the one almost nobody graphs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Practical version: for every pool in your system — database connections, HTTP clients, thread pools, worker queues — export the &lt;strong&gt;queue depth and the wait time&lt;/strong&gt; , not just the in-use count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your load test is lying to you
&lt;/h2&gt;

&lt;p&gt;This one deserves its own section, because it invalidates the measurement most teams trust most.&lt;/p&gt;

&lt;p&gt;Most load generators work in a &lt;strong&gt;closed loop&lt;/strong&gt; : send a request, wait for the response, send the next one. It seems reasonable. It produces latency numbers that are quietly, systematically wrong.&lt;/p&gt;

&lt;p&gt;Picture a generator with 100 virtual users hammering a service that stalls for 2 seconds. During that stall, each user is blocked waiting. They send &lt;strong&gt;nothing&lt;/strong&gt;. When the stall clears, everyone resumes and records a normal-looking latency.&lt;/p&gt;

&lt;p&gt;The result: the worst two seconds of your test contributed almost &lt;strong&gt;no samples&lt;/strong&gt;. Your p99 is computed over a set of measurements that excludes the period you actually care about.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;coordinated omission&lt;/strong&gt; , named by Gil Tene. The load generator has unwittingly coordinated with the system under test to only sample it when it is healthy.&lt;/p&gt;

&lt;p&gt;Real users do not behave that way. They arrive on their own schedule. A user who clicks during your 2-second stall waits the full 2 seconds — plus however long the queue in front of them takes to drain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to tell if you have it:&lt;/strong&gt; if your p99 under load is suspiciously close to your median, and your users report freezes you cannot reproduce, you almost certainly do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure against the intended schedule, not the actual one.&lt;/strong&gt; If you meant to send a request at t=0 and only sent it at t=1.8s because you were blocked, that request’s latency starts at t=0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a corrected tool.&lt;/strong&gt; &lt;code&gt;wrk2&lt;/code&gt; was written specifically for this. HdrHistogram provides &lt;code&gt;recordValueWithExpectedInterval&lt;/code&gt; to backfill the missing samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer an open model.&lt;/strong&gt; Generate load at a fixed arrival rate regardless of whether previous responses came back — which is what real traffic does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until you fix this, every latency number you have collected under saturation is an underestimate, and the worse the stall, the bigger the lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding it in practice
&lt;/h2&gt;

&lt;p&gt;Enough theory. Here is the actual sequence when latency is up and CPU is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with the database, because it usually is.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- What is everyone waiting on, right now?&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;

&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;A pile of rows in &lt;code&gt;Client / ClientRead&lt;/code&gt; means the database is idle and waiting on &lt;em&gt;you&lt;/em&gt; — the bottleneck is elsewhere. A pile in &lt;code&gt;Lock&lt;/code&gt; or &lt;code&gt;LWLock&lt;/code&gt; means contention inside the database. Many &lt;code&gt;idle in transaction&lt;/code&gt; means your application is holding connections open without using them, which is the same as having a smaller pool than you think.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- How much connection headroom is left?&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

       &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'max_connections'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_conn&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Then the sockets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
ss &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="c"&gt;# totals, including TIME_WAIT&lt;/span&gt;

ss &lt;span class="nt"&gt;-tan&lt;/span&gt; state time-wait | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="c"&gt;# how deep is the TIME_WAIT hole&lt;/span&gt;

ss &lt;span class="nt"&gt;-tan&lt;/span&gt; state established | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="c"&gt;# what you actually have open&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;If &lt;code&gt;TIME_WAIT&lt;/code&gt; is in the tens of thousands, you are not reusing connections. That is a client configuration problem, not a capacity problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the kernel tables.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_local_port_range

sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max

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

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Then the process limits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/limits | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'open files'&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/fd | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;The whole sequence takes about three minutes and will identify the binding constraint far more often than another dashboard will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;First thing to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low CPU, high latency&lt;/td&gt;
&lt;td&gt;Queuing on a pool&lt;/td&gt;
&lt;td&gt;Pool wait time, queue depth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors at N pods but not N-1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max_connections&lt;/code&gt; exceeded&lt;/td&gt;
&lt;td&gt;Pods × pool size vs DB limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;too many open files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FD limit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ulimit -n&lt;/code&gt;, &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/limits&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect timeouts to one downstream&lt;/td&gt;
&lt;td&gt;Ephemeral ports / TIME_WAIT&lt;/td&gt;
&lt;td&gt;Connection reuse, keep-alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent packet loss under load&lt;/td&gt;
&lt;td&gt;conntrack table full&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nf_conntrack_count&lt;/code&gt; vs &lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uneven backend CPU, even connections&lt;/td&gt;
&lt;td&gt;L4 balancing HTTP/2&lt;/td&gt;
&lt;td&gt;Move to request-level balancing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency grows, never recovers&lt;/td&gt;
&lt;td&gt;No backpressure&lt;/td&gt;
&lt;td&gt;Bound the queue, shed load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding pods makes it slower&lt;/td&gt;
&lt;td&gt;Shared-resource contention&lt;/td&gt;
&lt;td&gt;What do all pods compete for?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;CPU is the easiest thing to graph, which is why it is the first thing people look at and the last thing that is usually wrong.&lt;/p&gt;

&lt;p&gt;Requests queue for connections, descriptors, ports, locks and threads — and every one of those queues follows the same arithmetic. Concurrency equals arrival rate times latency. Know two terms and you know the third, which means you can calculate your ceiling instead of discovering it during an incident.&lt;/p&gt;

&lt;p&gt;Before adding capacity, answer one question: &lt;strong&gt;what is the request waiting for?&lt;/strong&gt; If you cannot name it, another instance will not fix it — it will just give the same queue more customers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/architecture/scalability-bottleneck-is-not-the-cpu-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>scalability</category>
      <category>loadbalancing</category>
      <category>distributedsystems</category>
      <category>connectionpool</category>
    </item>
    <item>
      <title>Skalierbarkeit: Der Engpass ist fast nie die CPU</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/skalierbarkeit-der-engpass-ist-fast-nie-die-cpu-1970</link>
      <guid>https://dev.to/aleksei_aleinikov/skalierbarkeit-der-engpass-ist-fast-nie-die-cpu-1970</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" alt="Skalierbarkeit: Der Engpass ist fast nie die CPU" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ihr Dashboard zeigt 28 Prozent CPU. Ihre p99-Latenz liegt bei vier Sekunden. Jemand schlägt vor, mehr Pods zu starten.&lt;/p&gt;

&lt;p&gt;Das ist die häufigste Fehlabbiegung bei Skalierungsarbeit, und sie folgt aus einer nachvollziehbaren Annahme: dass ein langsames System ein ausgelastetes System sein müsse. Meistens ist es das nicht. Es ist ein System, in dem Anfragen auf etwas warten, das keine CPU ist — und zusätzliche Instanzen machen es oft schlimmer.&lt;/p&gt;

&lt;p&gt;Hier steht, was Sie tatsächlich begrenzt, und die Rechnung, mit der Sie es finden.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0cs48nc6jduuqzqhdz3.webp" alt="Skalierbarkeits-Engpässe: warum die CPU untätig ist, während Anfragen anderswo warten." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Eine Formel, die fast alles dimensioniert
&lt;/h2&gt;

&lt;p&gt;Littles Gesetz ist das Nützlichste in der Kapazitätsplanung und passt in eine Zeile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nebenläufigkeit = Ankunftsrate x Latenz

L = λ × W
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Treffen 500 Anfragen pro Sekunde ein und verbringt jede 40 ms in der Datenbank, brauchen Sie zu jedem Zeitpunkt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 Anf./s × 0,04 s = 20 gleichzeitige Datenbankverbindungen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Zwanzig. Nicht zweihundert.&lt;/p&gt;

&lt;p&gt;Jetzt rückwärts — dort verdient die Formel ihr Geld. Sie haben einen Pool mit &lt;strong&gt;20 Verbindungen&lt;/strong&gt; , jede Abfrage dauert &lt;strong&gt;50 ms&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;20 / 0,05 s = 400 Anfragen pro Sekunde
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Das ist Ihre Decke.&lt;/strong&gt; Kein weiches Limit, ein hartes. Sie können vierzig Pods betreiben; der Pool lässt weiterhin 400 Anfragen pro Sekunde durch, alles andere wartet. Die CPU bleibt niedrig, weil die CPU nichts tut. Sie wartet.&lt;/p&gt;

&lt;p&gt;Jede Warteschlange in Ihrem System folgt dem. Thread-Pools, Connection-Pools, Worker-Zahlen, In-Flight-Limits. Kennen Sie zwei der drei Größen, kennen Sie die dritte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum 80 Prozent Auslastung nicht zu 80 Prozent in Ordnung sind
&lt;/h2&gt;

&lt;p&gt;Hier steht der Teil, der erklärt, warum Kapazitätsplanung unintuitiv wirkt.&lt;/p&gt;

&lt;p&gt;Bei einer einfachen Warteschlange hängt die Antwortzeit so von der Auslastung ab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Antwortzeit = Bedienzeit / (1 - Auslastung)

R = S / (1 - ρ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Der Nenner ist die ganze Geschichte. Nähert sich die Auslastung der Eins, steigt die Antwortzeit nicht sanft an — sie geht gegen unendlich.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Auslastung&lt;/th&gt;
&lt;th&gt;Antwortzeit gegenüber Leerlauf&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Der Schritt von 50 auf 80 Prozent Auslastung kostet Sie nicht 30 Prozent Ihres Latenzbudgets. Er kostet &lt;strong&gt;150 Prozent&lt;/strong&gt; — die Antwortzeit springt vom Doppelten auf das Fünffache der Bedienzeit.&lt;/p&gt;

&lt;p&gt;Deshalb halten erfahrene Teams scheinbar verschwenderische Reserven vor. Und deshalb fällt ein System, das gestern bei 70 Prozent stabil lief, heute bei 85 Prozent nach einem moderaten Lastanstieg um. Geändert hat sich nichts außer Ihrer Position auf einer Kurve, die senkrecht wird.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Die ehrliche Einschränkung:&lt;/strong&gt; Diese Formel setzt einen einzelnen Server mit Poisson-Ankünften und exponentialverteilten Bedienzeiten voraus — eine M/M/1-Warteschlange. Reale Systeme haben mehrere Server und stößeren Verkehr, Ihre Konstanten werden also abweichen. Die &lt;em&gt;Form&lt;/em&gt; aber ist universell: Wartezeit ist hyperbolisch in der Auslastung, nicht linear. Jede Ihrer Warteschlangen hat ein Knie, und es liegt weiter links, als die Intuition nahelegt.&lt;/p&gt;

&lt;p&gt;Praktische Folge: &lt;strong&gt;hören Sie auf, Autoscaling-Ziele auf 80 Prozent CPU zu setzen.&lt;/strong&gt; Dort sind Sie bereits tief in der Kurve, und die neue Instanz braucht Zeit, bis sie nützt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Decken, in der Reihenfolge ihres Auftretens
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Datenbankverbindungen
&lt;/h3&gt;

&lt;p&gt;Das häufigste echte Limit — und es hat eine Zahl, die Sie nachschlagen können.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL erlaubt standardmäßig &lt;code&gt;max_connections = 100&lt;/code&gt;&lt;/strong&gt; , davon sind &lt;strong&gt;drei für Superuser reserviert&lt;/strong&gt;. Für Ihre Anwendung bleiben also rund 97.&lt;/p&gt;

&lt;p&gt;Zählen Sie nun, was Sie anfordern. Zwölf Pods mit je zehn Verbindungen sind 120 gegen ein Limit von 97. Der zwölfte Pod bekommt Verbindungsfehler, und der Fehler sieht aus wie ein Datenbankproblem statt wie ein Rechenfehler.&lt;/p&gt;

&lt;p&gt;Zwei Dinge verschärfen das:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL &lt;strong&gt;forkt einen Prozess pro Verbindung&lt;/strong&gt; , Verbindungen kosten also echten Speicher. Die Dokumentation sagt ausdrücklich, dass ein höheres &lt;code&gt;max_connections&lt;/code&gt; auch die Shared-Memory-Zuteilung erhöht. Genau dafür gibt es PgBouncer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ein größerer Pool ist nicht mehr Durchsatz.&lt;/strong&gt; Jenseits des Punktes, an dem die Datenbank Abfragen wirklich parallel ausführen kann, bringen zusätzliche Verbindungen Lock-Contention und Context-Switches. Der Durchsatz flacht ab und fällt dann.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MySQL nutzt einen Thread pro Verbindung und ist hier günstiger — einer der &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/data/postgres-vs-mysql-2026-vergleich" rel="noopener noreferrer"&gt;strukturellen Unterschiede zwischen Postgres und MySQL&lt;/a&gt;, den man vor der Wahl kennen sollte.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dateideskriptoren
&lt;/h3&gt;

&lt;p&gt;Jeder Socket, jede offene Datei, jede Verbindung ist ein Dateideskriptor. Das Limit pro Prozess liegt beim Soft-Limit oft bei &lt;strong&gt;1024&lt;/strong&gt; — klingt großzügig, bis man merkt, dass es eingehende Verbindungen, ausgehende Verbindungen, Logdateien und Bibliotheks-Interna gleichzeitig abdeckt.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;ulimit&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="c"&gt;# Soft-Limit dieser Shell&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/limits &lt;span class="c"&gt;# was der laufende Prozess wirklich hat&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/fd | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="c"&gt;# wie viele er gerade nutzt&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Der Fehlermodus ist einprägsam: &lt;code&gt;EMFILE: too many open files&lt;/code&gt;, meist genau unter der Last, bei der man am wenigsten Lust auf einen Stacktrace hat.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ephemere Ports und TIME_WAIT
&lt;/h3&gt;

&lt;p&gt;Dieser Punkt überrascht, weil nichts in der Anwendung ihn erwähnt.&lt;/p&gt;

&lt;p&gt;Öffnet Ihr Dienst eine ausgehende Verbindung, wählt der Kernel einen Quellport aus dem ephemeren Bereich. Unter Linux:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_local_port_range

&lt;span class="c"&gt;# typischerweise: 32768 60999&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Das sind rund &lt;strong&gt;28.000 Ports&lt;/strong&gt; je eindeutiger Kombination aus Zieladresse und Zielport.&lt;/p&gt;

&lt;p&gt;Anschließend hält die schließende Seite den Socket &lt;strong&gt;60 Sekunden im TIME_WAIT&lt;/strong&gt; — und unter Linux ist diese Dauer in den Kernel einkompiliert, kein sysctl, das Sie herunterdrehen können.&lt;/p&gt;

&lt;p&gt;Rechnen Sie nach. 500 kurzlebige Verbindungen pro Sekunde zum selben nachgelagerten Dienst:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 Verb./s × 60 s = 30.000 Sockets im TIME_WAIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Der Bereich ist aufgebraucht. Neue Verbindungen scheitern, und der Fehler ist ein Connect-Timeout, der aussieht, als sei der andere Dienst ausgefallen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Die Lösung ist nicht, den Bereich zu vergrößern.&lt;/strong&gt; Sie ist, die Verbindungen gar nicht erst zu öffnen: HTTP-Keep-Alive, Connection-Pooling, HTTP/2-Multiplexing. Wiederverwendung schlägt Bereitstellung.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Connection Tracking
&lt;/h3&gt;

&lt;p&gt;Läuft Ihr Verkehr über ein NAT-Gateway, eine Firewall oder einen Kubernetes-Node mit iptables, führt der Kernel für jeden Flow einen &lt;strong&gt;conntrack&lt;/strong&gt; -Eintrag. Diese Tabelle ist endlich, und wenn sie voll ist, werden Pakete stillschweigend verworfen — kein Fehler, keine Logzeile in Ihrer Anwendung, nur Latenz und Retries.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
sysctl net.netfilter.nf_conntrack_max

sysctl net.netfilter.nf_conntrack_count

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

&lt;/div&gt;





&lt;p&gt;Der Abstand zwischen diesen beiden Zahlen ist eine Alarmschwelle wert — und fast niemand exportiert sie.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Threads und Event Loops
&lt;/h3&gt;

&lt;p&gt;Ein Server mit einem Thread pro Anfrage ist ein Pool unter anderem Namen, Littles Gesetz gilt unverändert. Ein Event-Loop-Server hat dieses Limit nicht — was seine eigene Gefahr ist, denn er nimmt bereitwillig zehntausend gleichzeitige Anfragen an und bedient alle langsam, statt einige schnell abzulehnen.&lt;/p&gt;

&lt;p&gt;Ein einziger blockierender Aufruf im Event Loop legt jede Anfrage auf diesem Loop lahm. Der CPU-Graph bleibt dabei ruhig.&lt;/p&gt;

&lt;h2&gt;
  
  
  Horizontale Skalierung und der gemeinsame Nenner
&lt;/h2&gt;

&lt;p&gt;Zusätzliche Instanzen vervielfachen die Kapazität für den &lt;strong&gt;unabhängigen&lt;/strong&gt; Teil einer Anfrage und ändern nichts am geteilten Teil.&lt;/p&gt;

&lt;p&gt;Amdahls Gesetz deckelt den Gewinn aus Parallelisierung bei allem, was einen seriellen Anteil hat. Das &lt;strong&gt;Universal Scalability Law&lt;/strong&gt; ergänzt den Teil, den die meisten übersehen — und es lohnt sich, ihn auszuschreiben, denn der zweite Term ist der gefährliche:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C(N) = N / (1 + σ(N-1) + κN(N-1))

  N Anzahl der Worker

  σ Contention — serieller Anteil, Warten auf geteilte Ressourcen

  κ Coherency — Kosten, die Worker untereinander konsistent zu halten

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

&lt;/div&gt;





&lt;p&gt;Mit κ = 0 wird daraus Amdahls Gesetz: Der Durchsatz flacht ab, fällt aber nie. Mit &lt;strong&gt;κ &amp;gt; 0&lt;/strong&gt; passiert etwas Schlimmeres — es gibt ein Maximum, und dahinter &lt;strong&gt;sinkt&lt;/strong&gt; der Durchsatz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum bei N* = sqrt((1 - σ) / κ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Der Coherency-Term erklärt, warum zusätzliche Kapazität ein System verlangsamen kann. Er umfasst Cache-Line-Bouncing zwischen Kernen, Lock-Übergaben, Cluster-Gossip, verteilten Konsens und regionsübergreifende Replikation — alles, dessen Kosten mit dem &lt;em&gt;Quadrat&lt;/em&gt; der Teilnehmerzahl wachsen, weil jeder sich mit jedem abstimmen muss.&lt;/p&gt;

&lt;p&gt;Konkret: Wenn jeder neue Pod weitere Verbindungen zur selben Primärdatenbank öffnet, skalieren Sie nicht. Sie verwandeln ein Verbindungslimit in eine Warteschlange und danach in einen Timeout. Wenn jeder neue Knoten mit jedem anderen gossipen muss, haben Sie sich einen κ-Term gekauft.&lt;/p&gt;

&lt;p&gt;Fragen Sie vor jeder zusätzlichen Instanz:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Was berührt eine Anfrage, das &lt;strong&gt;geteilt&lt;/strong&gt; ist?&lt;/li&gt;
&lt;li&gt;Wie hoch ist das aktuelle Nebenläufigkeitslimit dieser geteilten Sache?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erhöht&lt;/strong&gt; eine weitere Instanz dieses Limit oder &lt;strong&gt;konkurriert&lt;/strong&gt; sie darum?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lautet die Antwort auf 3 „konkurriert”, macht Skalieren es schlimmer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load Balancing: was die Diagramme weglassen
&lt;/h2&gt;

&lt;p&gt;Der Kasten mit der Aufschrift „LB” verbirgt drei Entscheidungen, die darüber bestimmen, ob Skalierung funktioniert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health Checks, die lügen.&lt;/strong&gt; Eine Prüfung, die 200 zurückgibt, sobald der Prozess lebt, leitet Verkehr weiter in eine Instanz, deren Datenbank-Pool erschöpft und deren Warteschlange dreißig Sekunden tief ist. Eine nützliche Prüfung verifiziert die Abhängigkeiten, die eine Anfrage wirklich braucht — und trennt &lt;em&gt;Liveness&lt;/em&gt; (starte mich neu) von &lt;em&gt;Readiness&lt;/em&gt; (schick mir keinen Verkehr). Beides zu vermischen erzeugt Neustartschleifen unter Last, also genau dann, wenn Sie Stabilität brauchen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L4 gegen L7 unter HTTP/2.&lt;/strong&gt; L4 verteilt &lt;strong&gt;Verbindungen&lt;/strong&gt;. Bei HTTP/2 oder gRPC trägt eine langlebige Verbindung viele gemultiplexte Anfragen — Verteilung auf Verbindungsebene kann einem Backend also den Großteil der Arbeit überlassen, während die übrigen nichts tun. Der Graph zeigt gleichmäßige Verbindungszahlen und völlig ungleiche CPU. Für HTTP/2 und gRPC wollen Sie &lt;strong&gt;Verteilung auf Anfrageebene&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky Sessions.&lt;/strong&gt; Einen Nutzer an eine Instanz zu binden hebelt die Verteilung absichtlich aus. Außerdem verliert ein Neustart den Zustand dieser Nutzer, und ein heißer Nutzer wird zu einer heißen Instanz, die Sie nicht verteilen können.&lt;/p&gt;

&lt;p&gt;Dazu die Retry-Falle: Ist ein System bereits gesättigt, vervielfachen automatische Wiederholungen genau die Last, die es gesättigt hat. Retries brauchen &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/architecture/rate-limiting-in-der-praxis-algorithmen-header-2026" rel="noopener noreferrer"&gt;Rate Limiting und ein Budget&lt;/a&gt; sowie Jitter — sonst wird aus einem Brown-out ein Ausfall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warteschlangen puffern Spitzen, nicht Überlast
&lt;/h2&gt;

&lt;p&gt;Eine Warteschlange vor einem langsamen Konsumenten verschafft Ihnen Zeit über eine &lt;strong&gt;Spitze&lt;/strong&gt; hinweg. Gegen eine dauerhaft über der Bedienrate liegende Ankunftsrate hilft sie nicht — sie verschiebt nur, wo das Scheitern sichtbar wird.&lt;/p&gt;

&lt;p&gt;Übersteigen die Ankünfte die Bedienkapazität auf Dauer, wächst die Warteschlange unbegrenzt und die Latenz mit ihr. Aufrufer laufen in Timeouts. Sie wiederholen. Die Ankünfte steigen. Das ist eine Rückkopplung mit falschem Vorzeichen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backpressure&lt;/strong&gt; ist die Antwort, und sie bedeutet, Nein sagen zu können:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Begrenzte Warteschlangen.&lt;/strong&gt; Eine unbegrenzte Warteschlange ist ein Out-of-Memory mit Zwischenschritten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schnell ablehnen.&lt;/strong&gt; Ein 429 mit &lt;code&gt;Retry-After&lt;/code&gt; nach 5 ms ist deutlich freundlicher als ein Timeout nach 30 Sekunden — für den Aufrufer und für Sie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nach Wert verwerfen.&lt;/strong&gt; Unter Druck kippen Sie den Batch-Export, bevor Sie den Checkout kippen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Flight-Arbeit deckeln.&lt;/strong&gt; Nebenläufigkeitslimits je Abhängigkeit verhindern, dass ein langsamer Downstream alle Worker verbraucht.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Der Reflex sagt, Anfragen abzulehnen sei Scheitern. Die Alternative ist, Anfragen anzunehmen, die Sie nicht bedienen können — das ist ebenfalls Scheitern, nur später und für alle gleichzeitig.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was Multi-Region leistet und was nicht
&lt;/h2&gt;

&lt;p&gt;Multi-Region kauft Ihnen &lt;strong&gt;Verfügbarkeit&lt;/strong&gt; beim Ausfall einer Region und &lt;strong&gt;niedrigere Latenz&lt;/strong&gt; für Nutzer in der Nähe einer Region.&lt;/p&gt;

&lt;p&gt;Es kauft Ihnen keinen Durchsatz für irgendetwas, das konsistent bleiben muss. Ein global konsistenter Schreibvorgang kostet mindestens einen regionsübergreifenden Roundtrip, und die Lichtgeschwindigkeit ist kein Konfigurationsparameter — grob 5 ms je 1.000 km in Glasfaser, bevor irgendein Gerät das Paket anfasst. Frankfurt nach Virginia hat eine physikalische Untergrenze von mehreren Dutzend Millisekunden pro Roundtrip.&lt;/p&gt;

&lt;p&gt;Kreuzt Ihr Schreibpfad Regionen, haben Sie Schreibvorgänge nicht skaliert. Sie haben Latenz und einen Partitionsmodus zum Testen hinzugefügt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Den echten Engpass finden
&lt;/h2&gt;

&lt;p&gt;Brendan Greggs &lt;strong&gt;USE-Methode&lt;/strong&gt; ist der schnellste Weg. Messen Sie für jede Ressource drei Dinge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Was es bedeutet&lt;/th&gt;
&lt;th&gt;Warum es zählt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Utilisation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anteil der Zeit, in der die Ressource beschäftigt ist&lt;/td&gt;
&lt;td&gt;Für sich genommen irreführend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Saturation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wie viel Arbeit &lt;strong&gt;wartet&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Meist das eigentliche Signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Errors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ablehnungen, Timeouts, Verwürfe&lt;/td&gt;
&lt;td&gt;Sagt, was bereits kaputt ist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Die meisten Teams exportieren Auslastung und hören dort auf. Aber ein Pool mit 100 Prozent Auslastung und leerer Warteschlange ist kerngesund, während ein Pool mit 60 Prozent und einer dreißig Einträge tiefen Warteschlange Ihr Ausfall ist. &lt;strong&gt;Die Sättigung benennt den Engpass — und genau sie zeichnet fast niemand auf.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Praktisch: Exportieren Sie für jeden Pool im System — Datenbankverbindungen, HTTP-Clients, Thread-Pools, Worker-Queues — &lt;strong&gt;Warteschlangentiefe und Wartezeit&lt;/strong&gt; , nicht nur die Zahl der belegten Slots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ihr Lasttest lügt Sie an
&lt;/h2&gt;

&lt;p&gt;Dieser Punkt verdient einen eigenen Abschnitt, denn er entwertet genau die Messung, der die meisten Teams am meisten vertrauen.&lt;/p&gt;

&lt;p&gt;Die meisten Lastgeneratoren arbeiten in einer &lt;strong&gt;geschlossenen Schleife&lt;/strong&gt; : Anfrage senden, auf die Antwort warten, nächste senden. Klingt vernünftig. Liefert Latenzzahlen, die still und systematisch falsch sind.&lt;/p&gt;

&lt;p&gt;Stellen Sie sich einen Generator mit 100 virtuellen Nutzern vor, der einen Dienst belastet, welcher zwei Sekunden hängt. Während dieses Hängers ist jeder Nutzer blockiert. Sie senden &lt;strong&gt;nichts&lt;/strong&gt;. Löst sich der Stau, machen alle weiter und verbuchen eine normal aussehende Latenz.&lt;/p&gt;

&lt;p&gt;Das Ergebnis: Die schlimmsten zwei Sekunden Ihres Tests haben fast &lt;strong&gt;keine Messpunkte&lt;/strong&gt; beigesteuert. Ihr p99 wird über eine Menge berechnet, die genau den Zeitraum ausschließt, der Sie interessiert.&lt;/p&gt;

&lt;p&gt;Das ist &lt;strong&gt;Coordinated Omission&lt;/strong&gt; , benannt von Gil Tene. Der Lastgenerator hat sich unfreiwillig mit dem Testsystem abgestimmt und misst es nur dann, wenn es gesund ist.&lt;/p&gt;

&lt;p&gt;Echte Nutzer verhalten sich nicht so. Sie kommen nach eigenem Fahrplan. Wer während Ihres Zwei-Sekunden-Hängers klickt, wartet die vollen zwei Sekunden — plus die Zeit, die die Warteschlange davor zum Abbau braucht.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Woran Sie es erkennen:&lt;/strong&gt; Liegt Ihr p99 unter Last verdächtig nah am Median, während Nutzer von Einfrierern berichten, die Sie nicht reproduzieren können, haben Sie es mit ziemlicher Sicherheit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Die Gegenmittel:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gegen den geplanten Fahrplan messen, nicht gegen den tatsächlichen.&lt;/strong&gt; Wollten Sie bei t=0 senden und kamen blockiert erst bei t=1,8 s dazu, beginnt die Latenz dieser Anfrage bei t=0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ein korrigiertes Werkzeug nutzen.&lt;/strong&gt; &lt;code&gt;wrk2&lt;/code&gt; wurde genau dafür geschrieben. HdrHistogram bietet &lt;code&gt;recordValueWithExpectedInterval&lt;/code&gt;, um die fehlenden Messpunkte nachzutragen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offenes Modell bevorzugen.&lt;/strong&gt; Last mit fester Ankunftsrate erzeugen, unabhängig davon, ob vorherige Antworten zurückkamen — genau so verhält sich echter Verkehr.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bis Sie das beheben, ist jede unter Sättigung erhobene Latenzzahl eine Untertreibung — und je schlimmer der Hänger, desto größer die Lüge.&lt;/p&gt;

&lt;h2&gt;
  
  
  In der Praxis finden
&lt;/h2&gt;

&lt;p&gt;Genug Theorie. Hier die tatsächliche Reihenfolge, wenn die Latenz oben ist und die CPU nicht.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bei der Datenbank anfangen, denn dort liegt es meistens.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Worauf wartet gerade wer?&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;

&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Viele Zeilen mit &lt;code&gt;Client / ClientRead&lt;/code&gt; bedeuten, dass die Datenbank untätig ist und auf &lt;em&gt;Sie&lt;/em&gt; wartet — der Engpass liegt woanders. Viele &lt;code&gt;Lock&lt;/code&gt; oder &lt;code&gt;LWLock&lt;/code&gt; bedeuten Contention innerhalb der Datenbank. Viele &lt;code&gt;idle in transaction&lt;/code&gt; bedeuten, dass Ihre Anwendung Verbindungen offen hält, ohne sie zu nutzen — das entspricht einem kleineren Pool, als Sie denken.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Wie viel Verbindungsreserve ist übrig?&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

       &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'max_connections'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_conn&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Dann die Sockets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
ss &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="c"&gt;# Summen, inklusive TIME_WAIT&lt;/span&gt;

ss &lt;span class="nt"&gt;-tan&lt;/span&gt; state time-wait | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="c"&gt;# wie tief ist das TIME_WAIT-Loch&lt;/span&gt;

ss &lt;span class="nt"&gt;-tan&lt;/span&gt; state established | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="c"&gt;# was wirklich offen ist&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Liegt &lt;code&gt;TIME_WAIT&lt;/code&gt; im Zehntausenderbereich, verwenden Sie Verbindungen nicht wieder. Das ist ein Client-Konfigurationsproblem, kein Kapazitätsproblem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dann die Kernel-Tabellen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_local_port_range

sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max

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

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Dann die Prozesslimits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/limits | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'open files'&lt;/span&gt;

&lt;span class="nb"&gt;ls&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/fd | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Die ganze Sequenz dauert etwa drei Minuten und benennt die bindende Randbedingung deutlich häufiger als ein weiteres Dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Checkliste
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Wahrscheinliche Ursache&lt;/th&gt;
&lt;th&gt;Was zuerst prüfen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Niedrige CPU, hohe Latenz&lt;/td&gt;
&lt;td&gt;Warten an einem Pool&lt;/td&gt;
&lt;td&gt;Wartezeit im Pool, Warteschlangentiefe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fehler bei N Pods, nicht bei N-1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max_connections&lt;/code&gt; überschritten&lt;/td&gt;
&lt;td&gt;Pods × Poolgröße gegen DB-Limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;too many open files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FD-Limit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ulimit -n&lt;/code&gt;, &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/limits&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect-Timeouts zu einem Downstream&lt;/td&gt;
&lt;td&gt;Ephemere Ports / TIME_WAIT&lt;/td&gt;
&lt;td&gt;Wiederverwendung, Keep-Alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stiller Paketverlust unter Last&lt;/td&gt;
&lt;td&gt;conntrack-Tabelle voll&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nf_conntrack_count&lt;/code&gt; gegen &lt;code&gt;max&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ungleiche Backend-CPU, gleiche Verbindungen&lt;/td&gt;
&lt;td&gt;L4 verteilt HTTP/2&lt;/td&gt;
&lt;td&gt;Auf Anfrageebene verteilen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latenz wächst und erholt sich nie&lt;/td&gt;
&lt;td&gt;Keine Backpressure&lt;/td&gt;
&lt;td&gt;Warteschlange begrenzen, Last abwerfen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mehr Pods machen es langsamer&lt;/td&gt;
&lt;td&gt;Contention um geteilte Ressourcen&lt;/td&gt;
&lt;td&gt;Worum konkurrieren alle Pods?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Das Fazit
&lt;/h2&gt;

&lt;p&gt;CPU ist am leichtesten zu graphen — deshalb schaut man zuerst dorthin, und deshalb ist es meist das Falsche.&lt;/p&gt;

&lt;p&gt;Anfragen warten auf Verbindungen, Deskriptoren, Ports, Locks und Threads, und jede dieser Warteschlangen folgt derselben Rechnung. Nebenläufigkeit gleich Ankunftsrate mal Latenz. Kennen Sie zwei Größen, kennen Sie die dritte — und können Ihre Decke ausrechnen, statt sie während eines Incidents zu entdecken.&lt;/p&gt;

&lt;p&gt;Beantworten Sie vor jeder zusätzlichen Kapazität eine Frage: &lt;strong&gt;Worauf wartet die Anfrage?&lt;/strong&gt; Können Sie es nicht benennen, behebt eine weitere Instanz nichts — sie verschafft derselben Warteschlange nur mehr Kundschaft.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ursprünglich veröffentlicht auf &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/architecture/skalierbarkeit-der-engpass-ist-selten-die-cpu-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>skalierbarkeit</category>
      <category>loadbalancing</category>
      <category>verteiltesysteme</category>
      <category>connectionpool</category>
    </item>
    <item>
      <title>Postgres vs MySQL in 2026: Performance, Syntax and the Real Differences</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/postgres-vs-mysql-in-2026-performance-syntax-and-the-real-differences-26n4</link>
      <guid>https://dev.to/aleksei_aleinikov/postgres-vs-mysql-in-2026-performance-syntax-and-the-real-differences-26n4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" alt="Postgres vs MySQL in 2026: Performance, Syntax and the Real Differences" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most Postgres versus MySQL comparisons are a feature checklist from 2015 with the dates changed. Both databases have added most of what the other used to have, so the checklist tells you almost nothing.&lt;/p&gt;

&lt;p&gt;What still differs is deeper: how they store rows, how they handle concurrent versions of those rows, how they treat a connection, and what happens when a migration fails halfway. Those differences do not show up in a feature table, and they are the ones you live with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" alt="Postgres vs MySQL in 2026: storage, concurrency, connections and syntax compared." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex queries, analytics, JSON, geospatial, extensions?&lt;/strong&gt; Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Huge numbers of simple connections, key-based lookups, read replicas?&lt;/strong&gt; MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team already fluent in one of them?&lt;/strong&gt; That one. Seriously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting fresh with no constraint?&lt;/strong&gt; Postgres, because the extension ecosystem gives you more escape hatches later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single process, no network, no ops?&lt;/strong&gt; Neither — that is SQLite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything below is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually differs under the hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Storage: clustered vs heap
&lt;/h3&gt;

&lt;p&gt;This single choice explains most performance differences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL (InnoDB) clusters the table on the primary key.&lt;/strong&gt; The row data physically lives inside the primary key index. Look up by primary key and you traverse one B-tree and you are holding the row.&lt;/p&gt;

&lt;p&gt;The cost: &lt;strong&gt;secondary indexes store the primary key, not a row location&lt;/strong&gt;. So a lookup via a secondary index traverses that index to find a primary key, then traverses the primary key index to find the row. Two traversals. It also means a wide primary key inflates every secondary index on the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres stores rows in a heap&lt;/strong&gt; — an unordered pile of pages — and every index, including the primary key, points into it. Every index lookup costs an extra fetch to reach the row.&lt;/p&gt;

&lt;p&gt;The benefit: all indexes are equal, so a wide primary key does not tax the others, and Postgres can use &lt;strong&gt;index-only scans&lt;/strong&gt; when its visibility map says a page is fully visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means in practice:&lt;/strong&gt; MySQL is excellent at “give me the row with this ID”. Postgres is more even-handed when your access patterns are varied.&lt;/p&gt;

&lt;h3&gt;
  
  
  MVCC: where old row versions live
&lt;/h3&gt;

&lt;p&gt;Both give you readers that never block writers. They pay for it differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres writes a new version of the row into the table itself&lt;/strong&gt; and leaves the old one behind until &lt;code&gt;VACUUM&lt;/code&gt; reclaims it. Heavy-update tables therefore &lt;strong&gt;bloat&lt;/strong&gt; , and autovacuum keeping up is a real operational concern rather than a theoretical one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL writes old versions into undo logs&lt;/strong&gt; , so the table stays compact. The cost moves elsewhere: long-running transactions force the undo log to grow, and a reader on an old snapshot has to walk back through those versions.&lt;/p&gt;

&lt;p&gt;Neither approach is free. Postgres makes you think about vacuum; MySQL makes you think about long transactions and undo growth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connections: process vs thread
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Postgres forks a process per connection.&lt;/strong&gt; A process is not cheap. A few hundred mostly-idle connections consume real memory, and you will want &lt;strong&gt;PgBouncer&lt;/strong&gt; or an equivalent pooler in front of any serious deployment. Serverless platforms that open a connection per request make this acute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL uses a thread per connection&lt;/strong&gt; , which is dramatically cheaper. Thousands of connections is an ordinary configuration.&lt;/p&gt;

&lt;p&gt;If your architecture opens many short-lived connections and you cannot pool, that fact alone may pick your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres vs MySQL performance
&lt;/h2&gt;

&lt;p&gt;Nobody can honestly tell you which is faster, because they are fast at different shapes of work. Here is what each actually favours.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagyjp2ki1qvd1bgmfa46.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagyjp2ki1qvd1bgmfa46.webp" alt="Where Postgres and MySQL win: clustered vs heap storage and their different MVCC costs." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL tends to win at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary-key point lookups — one traversal, row in hand&lt;/li&gt;
&lt;li&gt;Very high connection counts&lt;/li&gt;
&lt;li&gt;Simple, high-volume OLTP where the query plan is obvious&lt;/li&gt;
&lt;li&gt;Read-scaling via replicas, with a long-mature binlog replication story&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres tends to win at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex joins, subqueries and aggregations — a stronger planner&lt;/li&gt;
&lt;li&gt;Analytical queries alongside transactional ones&lt;/li&gt;
&lt;li&gt;Anything with partial, expression or covering indexes&lt;/li&gt;
&lt;li&gt;Write patterns that benefit from &lt;code&gt;HOT&lt;/code&gt; updates avoiding index churn&lt;/li&gt;
&lt;li&gt;JSON, arrays, geospatial and vector work, because of extensions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A word on benchmarks.&lt;/strong&gt; Any number you read — including any I could publish — is one workload, one schema, one hardware profile and one configuration. Tuning moves results more than the engine choice does. If performance is genuinely your deciding factor, benchmark &lt;em&gt;your&lt;/em&gt; queries on &lt;em&gt;your&lt;/em&gt; data. Everything else is someone else’s workload.&lt;/p&gt;

&lt;p&gt;If you are optimising rather than choosing, the techniques in &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/data/sql-query-optimization-2026-faster-database-performance" rel="noopener noreferrer"&gt;SQL query optimisation&lt;/a&gt; apply to both engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres vs MySQL syntax differences
&lt;/h2&gt;

&lt;p&gt;This is the section that matters when you are porting something. These are the ones that actually break.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String concatenation&lt;/td&gt;
&lt;td&gt;`'a'&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quote an identifier&lt;/td&gt;
&lt;td&gt;{% raw %}&lt;code&gt;"my table"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;my table&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-incrementing key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GENERATED ALWAYS AS IDENTITY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AUTO_INCREMENT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upsert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return the inserted row&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... RETURNING *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;real &lt;code&gt;BOOLEAN&lt;/code&gt; type&lt;/td&gt;
&lt;td&gt;alias for &lt;code&gt;TINYINT(1)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String comparison&lt;/td&gt;
&lt;td&gt;case-sensitive by default&lt;/td&gt;
&lt;td&gt;case-insensitive in common collations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limit with offset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LIMIT 10 OFFSET 20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LIMIT 10 OFFSET 20&lt;/code&gt; or &lt;code&gt;LIMIT 20, 10&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current timestamp&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NOW()&lt;/code&gt;, &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NOW()&lt;/code&gt;, &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arrays&lt;/td&gt;
&lt;td&gt;native array types&lt;/td&gt;
&lt;td&gt;not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex match&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~&lt;/code&gt;, &lt;code&gt;~*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;REGEXP&lt;/code&gt;, &lt;code&gt;RLIKE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of these cause most of the pain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt;.&lt;/strong&gt; Postgres hands you back the row you just wrote in the same statement. MySQL does not support it, so you insert and then select, which is a second round trip and a race unless you are careful. MariaDB does support it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres: one statement&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a@b.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Case sensitivity.&lt;/strong&gt; In Postgres, &lt;code&gt;WHERE email = 'Bob@x.com'&lt;/code&gt; will not match a stored &lt;code&gt;bob@x.com&lt;/code&gt;. In MySQL with a typical collation, it will. Ports in either direction silently change behaviour, and the bug surfaces in production as “login sometimes doesn’t work”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unquoted identifier folding.&lt;/strong&gt; Postgres lowercases unquoted identifiers, so &lt;code&gt;CREATE TABLE MyTable&lt;/code&gt; produces &lt;code&gt;mytable&lt;/code&gt;. Once you quote something as &lt;code&gt;"MyTable"&lt;/code&gt;, you must quote it forever. The usual advice — stick to &lt;code&gt;lower_snake_case&lt;/code&gt; and never quote — is worth following in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations: the difference nobody mentions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Postgres has transactional DDL.&lt;/strong&gt; You can wrap schema changes in a transaction and roll them back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- something fails here&lt;/span&gt;

&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- the column never existed&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;MySQL commits implicitly on DDL.&lt;/strong&gt; A migration that fails on step four of six leaves you with three applied changes and no way back except a hand-written down migration.&lt;/p&gt;

&lt;p&gt;If you deploy schema changes often, this is a bigger day-to-day quality-of-life difference than any query benchmark.&lt;/p&gt;

&lt;p&gt;Two more operational notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extensions.&lt;/strong&gt; Postgres lets you add PostGIS for geospatial, pgvector for embeddings, TimescaleDB for time series — inside the same database, with the same backups and the same transactions. MySQL has no comparable mechanism. If you might need &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/data/do-you-need-a-vector-database-postgres-vs-dedicated-2026" rel="noopener noreferrer"&gt;a vector database&lt;/a&gt;, this matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication.&lt;/strong&gt; MySQL’s binlog replication is old, well understood and has excellent tooling. Postgres has streaming physical replication and logical replication; both work well, and logical replication has improved substantially in recent releases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where SQLite fits
&lt;/h2&gt;

&lt;p&gt;SQLite is frequently put in this comparison, and the framing is usually wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite is not a small server database. It is a library that runs inside your process.&lt;/strong&gt; There is no server, no port, no user accounts and no network round trip — a function call reads a file.&lt;/p&gt;

&lt;p&gt;That makes it excellent for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application-local state, caches and config&lt;/li&gt;
&lt;li&gt;Test suites, where a database per test costs microseconds&lt;/li&gt;
&lt;li&gt;Edge and embedded deployments&lt;/li&gt;
&lt;li&gt;Read-heavy workloads served from a single machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And unsuitable the moment you need &lt;strong&gt;several application servers writing concurrently&lt;/strong&gt;. SQLite serialises writers: one at a time.&lt;/p&gt;

&lt;p&gt;The honest rule: if exactly one process ever touches the data, SQLite is likely the right answer and the simplest one. If two might, you want a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  MariaDB vs MySQL
&lt;/h2&gt;

&lt;p&gt;MariaDB began as a MySQL fork after Oracle’s acquisition. Years later, “drop-in replacement” is no longer accurate.&lt;/p&gt;

&lt;p&gt;They have &lt;strong&gt;diverged&lt;/strong&gt; : separate optimiser development, different storage engines, and features on each side the other lacks. MariaDB has &lt;code&gt;RETURNING&lt;/code&gt;; MySQL does not. Version numbers no longer correspond at all.&lt;/p&gt;

&lt;p&gt;Drivers and basic dumps are largely interchangeable. &lt;strong&gt;Replication between them and any recent syntax are not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;MariaDB&lt;/strong&gt; for community governance and its specific feature set. Choose &lt;strong&gt;MySQL&lt;/strong&gt; if you depend on Oracle’s ecosystem, a managed offering built on it, or its exact replication semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which versions to run
&lt;/h2&gt;

&lt;p&gt;As of September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;18&lt;/strong&gt; (18.6)&lt;/td&gt;
&lt;td&gt;19 in beta; &lt;strong&gt;14 reaches EOL Nov 2026&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;9.7 LTS&lt;/strong&gt; (9.7.3)&lt;/td&gt;
&lt;td&gt;8.4 LTS supported to 2029; &lt;strong&gt;8.0 hit EOL Apr 2026&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MariaDB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;12.3 LTS&lt;/strong&gt; (12.3.3)&lt;/td&gt;
&lt;td&gt;11.8 and 11.4 LTS still supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things to act on: if you are on &lt;strong&gt;MySQL 8.0&lt;/strong&gt; , it is out of support — plan the move to 8.4 or 9.7. If you are on &lt;strong&gt;Postgres 14&lt;/strong&gt; , you have until November.&lt;/p&gt;

&lt;p&gt;Postgres ships a major version yearly with five years of support. MySQL splits into quarterly Innovation releases and roughly biennial LTS releases; unless you need a specific new feature, run LTS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complex queries, reporting alongside OLTP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Postgres&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geospatial, vectors, time series in one database&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — extensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequent schema migrations&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — transactional DDL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thousands of connections, no pooler possible&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MySQL&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key-value style access by primary key&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — clustered index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy read scaling via replicas&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — mature binlog tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One process owns the data, no network&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SQLite&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want community governance over Oracle&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MariaDB&lt;/strong&gt; or Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team already deeply fluent in one&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;That one&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The feature gap that made this an interesting argument a decade ago has mostly closed. What is left is structural: &lt;strong&gt;clustered versus heap storage&lt;/strong&gt; , &lt;strong&gt;old row versions in the table versus in undo logs&lt;/strong&gt; , &lt;strong&gt;a process versus a thread per connection&lt;/strong&gt; , and &lt;strong&gt;whether a failed migration can roll back&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those four differences will shape your operational life far more than any feature checklist. Pick on those, and on which one your team can debug at 3am.&lt;/p&gt;

&lt;p&gt;And if you find yourself deciding on a benchmark you did not run, you are choosing based on someone else’s workload.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/data/postgres-vs-mysql-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgresvsmysql</category>
      <category>postgres</category>
      <category>mysql</category>
      <category>databasecomparison</category>
    </item>
    <item>
      <title>Postgres vs. MySQL 2026: Performance, Syntax und die echten Unterschiede</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/postgres-vs-mysql-2026-performance-syntax-und-die-echten-unterschiede-3beb</link>
      <guid>https://dev.to/aleksei_aleinikov/postgres-vs-mysql-2026-performance-syntax-und-die-echten-unterschiede-3beb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" alt="Postgres vs. MySQL 2026: Performance, Syntax und die echten Unterschiede" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Die meisten Vergleiche von Postgres und MySQL sind eine Feature-Checkliste von 2015 mit aktualisierten Jahreszahlen. Beide Datenbanken haben inzwischen das meiste nachgeholt, was die jeweils andere früher exklusiv hatte — die Checkliste sagt also fast nichts mehr aus.&lt;/p&gt;

&lt;p&gt;Was sich weiterhin unterscheidet, liegt tiefer: wie Zeilen gespeichert werden, wie gleichzeitige Versionen dieser Zeilen verwaltet werden, was eine Verbindung kostet, und was passiert, wenn eine Migration auf halbem Weg scheitert. Diese Unterschiede tauchen in keiner Feature-Tabelle auf — und mit ihnen leben Sie täglich.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaiw640hzvxusjl70epv.webp" alt="Postgres vs. MySQL 2026: Speicherung, Nebenläufigkeit, Verbindungen und Syntax im Vergleich." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Antwort in 30 Sekunden
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Komplexe Abfragen, Analytik, JSON, Geodaten, Erweiterungen?&lt;/strong&gt; Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sehr viele einfache Verbindungen, Zugriffe über Schlüssel, Read-Replicas?&lt;/strong&gt; MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team beherrscht bereits eine der beiden?&lt;/strong&gt; Diese. Ernsthaft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grüne Wiese ohne Randbedingungen?&lt;/strong&gt; Postgres, weil das Erweiterungs-Ökosystem später mehr Notausgänge bietet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ein Prozess, kein Netzwerk, kein Betrieb?&lt;/strong&gt; Keine von beiden — das ist SQLite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alles Weitere ist die Begründung.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was sich unter der Haube wirklich unterscheidet
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Speicherung: geclustert gegen Heap
&lt;/h3&gt;

&lt;p&gt;Diese eine Entscheidung erklärt die meisten Performance-Unterschiede.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL (InnoDB) clustert die Tabelle über den Primärschlüssel.&lt;/strong&gt; Die Zeilendaten liegen physisch im Primärschlüsselindex. Ein Zugriff über den Primärschlüssel traversiert einen B-Baum — und Sie halten die Zeile in der Hand.&lt;/p&gt;

&lt;p&gt;Der Preis: &lt;strong&gt;Sekundärindizes speichern den Primärschlüssel, keine Zeilenposition&lt;/strong&gt;. Ein Zugriff über einen Sekundärindex traversiert also erst diesen Index bis zum Primärschlüssel und dann den Primärschlüsselindex bis zur Zeile. Zwei Traversierungen. Außerdem bläht ein breiter Primärschlüssel jeden Sekundärindex der Tabelle auf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres legt Zeilen in einem Heap ab&lt;/strong&gt; — einem ungeordneten Haufen von Seiten — und jeder Index, auch der Primärschlüssel, zeigt dort hinein. Jeder Indexzugriff kostet einen zusätzlichen Zugriff auf die Zeile.&lt;/p&gt;

&lt;p&gt;Der Vorteil: Alle Indizes sind gleichwertig, ein breiter Primärschlüssel belastet die übrigen nicht, und Postgres kann &lt;strong&gt;Index-Only-Scans&lt;/strong&gt; nutzen, wenn die Visibility Map eine Seite als vollständig sichtbar meldet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Praktisch heißt das:&lt;/strong&gt; MySQL ist hervorragend in „gib mir die Zeile mit dieser ID”. Postgres ist ausgewogener, wenn Ihre Zugriffsmuster vielfältig sind.&lt;/p&gt;

&lt;h3&gt;
  
  
  MVCC: wo alte Zeilenversionen liegen
&lt;/h3&gt;

&lt;p&gt;Beide liefern Leser, die Schreiber nie blockieren. Sie bezahlen es unterschiedlich.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres schreibt eine neue Zeilenversion in die Tabelle selbst&lt;/strong&gt; und lässt die alte liegen, bis &lt;code&gt;VACUUM&lt;/code&gt; sie freigibt. Änderungsintensive Tabellen &lt;strong&gt;blähen&lt;/strong&gt; dadurch auf, und ob Autovacuum hinterherkommt, ist eine reale Betriebsfrage, keine theoretische.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL schreibt alte Versionen in Undo-Logs&lt;/strong&gt; , die Tabelle bleibt also kompakt. Die Kosten wandern woandershin: Lang laufende Transaktionen lassen das Undo-Log wachsen, und ein Leser auf einem alten Snapshot muss sich durch diese Versionen zurückarbeiten.&lt;/p&gt;

&lt;p&gt;Keiner der beiden Wege ist gratis. Postgres zwingt Sie, über Vacuum nachzudenken; MySQL über lange Transaktionen und Undo-Wachstum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verbindungen: Prozess gegen Thread
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Postgres forkt einen Prozess pro Verbindung.&lt;/strong&gt; Ein Prozess ist nicht billig. Einige hundert überwiegend untätige Verbindungen kosten echten Speicher, und vor jedem ernsthaften Deployment wollen Sie &lt;strong&gt;PgBouncer&lt;/strong&gt; oder einen vergleichbaren Pooler. Serverless-Plattformen, die pro Request eine Verbindung öffnen, verschärfen das.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL nutzt einen Thread pro Verbindung&lt;/strong&gt; , was dramatisch günstiger ist. Tausende Verbindungen sind dort eine gewöhnliche Konfiguration.&lt;/p&gt;

&lt;p&gt;Wenn Ihre Architektur viele kurzlebige Verbindungen öffnet und Sie nicht poolen können, entscheidet allein diese Tatsache womöglich Ihre Datenbankwahl.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres vs. MySQL: Performance
&lt;/h2&gt;

&lt;p&gt;Niemand kann Ihnen ehrlich sagen, welche schneller ist, weil sie bei unterschiedlichen Arbeitsformen schnell sind. Was jede tatsächlich begünstigt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagyjp2ki1qvd1bgmfa46.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagyjp2ki1qvd1bgmfa46.webp" alt="Wo Postgres und MySQL gewinnen: geclusterte gegen Heap-Speicherung und ihre unterschiedlichen MVCC-Kosten." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL liegt tendenziell vorn bei:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Punktzugriffen über den Primärschlüssel — eine Traversierung, Zeile da&lt;/li&gt;
&lt;li&gt;sehr hohen Verbindungszahlen&lt;/li&gt;
&lt;li&gt;einfachem OLTP mit hohem Volumen, wo der Ausführungsplan offensichtlich ist&lt;/li&gt;
&lt;li&gt;Leseskalierung über Replicas, mit ausgereifter Binlog-Replikation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postgres liegt tendenziell vorn bei:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;komplexen Joins, Unterabfragen und Aggregationen — stärkerer Planer&lt;/li&gt;
&lt;li&gt;analytischen Abfragen neben dem transaktionalen Betrieb&lt;/li&gt;
&lt;li&gt;allem mit Teil-, Ausdrucks- oder abdeckenden Indizes&lt;/li&gt;
&lt;li&gt;Schreibmustern, die von &lt;code&gt;HOT&lt;/code&gt;-Updates ohne Index-Churn profitieren&lt;/li&gt;
&lt;li&gt;JSON, Arrays, Geodaten und Vektoren — dank Erweiterungen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ein Wort zu Benchmarks.&lt;/strong&gt; Jede Zahl, die Sie lesen — auch jede, die ich veröffentlichen könnte — ist eine Last, ein Schema, ein Hardwareprofil, eine Konfiguration. Tuning verschiebt Ergebnisse stärker als die Wahl der Engine. Wenn Performance wirklich Ihr Entscheidungskriterium ist, messen Sie &lt;em&gt;Ihre&lt;/em&gt; Abfragen auf &lt;em&gt;Ihren&lt;/em&gt; Daten. Alles andere ist die Last von jemand anderem.&lt;/p&gt;

&lt;p&gt;Wenn Sie nicht wählen, sondern optimieren: Die Techniken aus &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/data/sql-query-optimierung-2026-schnellere-datenbank-performance" rel="noopener noreferrer"&gt;SQL-Query-Optimierung&lt;/a&gt; gelten für beide Engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres vs. MySQL: Syntaxunterschiede
&lt;/h2&gt;

&lt;p&gt;Der Abschnitt, der bei einer Portierung zählt. Diese Punkte brechen tatsächlich.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thema&lt;/th&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String-Verkettung&lt;/td&gt;
&lt;td&gt;`'a'&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bezeichner quoten&lt;/td&gt;
&lt;td&gt;{% raw %}&lt;code&gt;"meine tabelle"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meine tabelle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-inkrementierender Schlüssel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GENERATED ALWAYS AS IDENTITY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AUTO_INCREMENT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upsert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eingefügte Zeile zurückgeben&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... RETURNING *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nicht unterstützt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;echter &lt;code&gt;BOOLEAN&lt;/code&gt;-Typ&lt;/td&gt;
&lt;td&gt;Alias für &lt;code&gt;TINYINT(1)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String-Vergleich&lt;/td&gt;
&lt;td&gt;standardmäßig case-sensitiv&lt;/td&gt;
&lt;td&gt;in üblichen Kollationen case-insensitiv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limit mit Offset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LIMIT 10 OFFSET 20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LIMIT 10 OFFSET 20&lt;/code&gt; oder &lt;code&gt;LIMIT 20, 10&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aktueller Zeitstempel&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NOW()&lt;/code&gt;, &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NOW()&lt;/code&gt;, &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arrays&lt;/td&gt;
&lt;td&gt;native Array-Typen&lt;/td&gt;
&lt;td&gt;nicht unterstützt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex-Vergleich&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~&lt;/code&gt;, &lt;code&gt;~*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;REGEXP&lt;/code&gt;, &lt;code&gt;RLIKE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Drei davon verursachen den meisten Schmerz:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt;.&lt;/strong&gt; Postgres gibt Ihnen die soeben geschriebene Zeile in derselben Anweisung zurück. MySQL unterstützt das nicht — Sie fügen ein und selektieren danach, also ein zweiter Roundtrip und ein Wettlauf, wenn Sie nicht aufpassen. MariaDB unterstützt es.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres: eine Anweisung&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a@b.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Groß-/Kleinschreibung.&lt;/strong&gt; In Postgres findet &lt;code&gt;WHERE email = 'Bob@x.com'&lt;/code&gt; ein gespeichertes &lt;code&gt;bob@x.com&lt;/code&gt; nicht. In MySQL mit üblicher Kollation schon. Portierungen in beide Richtungen ändern das Verhalten stillschweigend, und der Fehler zeigt sich in Produktion als „Login geht manchmal nicht”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faltung ungequoteter Bezeichner.&lt;/strong&gt; Postgres schreibt ungequotete Bezeichner klein, &lt;code&gt;CREATE TABLE MyTable&lt;/code&gt; erzeugt also &lt;code&gt;mytable&lt;/code&gt;. Sobald Sie etwas als &lt;code&gt;"MyTable"&lt;/code&gt; quoten, müssen Sie es für immer quoten. Der übliche Rat — durchgehend &lt;code&gt;lower_snake_case&lt;/code&gt;, niemals quoten — lohnt sich in beiden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Betrieb: der Unterschied, den niemand erwähnt
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Postgres beherrscht transaktionales DDL.&lt;/strong&gt; Sie können Schemaänderungen in eine Transaktion packen und zurückrollen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- hier scheitert etwas&lt;/span&gt;

&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- die Spalte hat nie existiert&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;MySQL committet bei DDL implizit.&lt;/strong&gt; Eine Migration, die bei Schritt vier von sechs scheitert, hinterlässt drei angewandte Änderungen und keinen Rückweg außer einer handgeschriebenen Down-Migration.&lt;/p&gt;

&lt;p&gt;Wer häufig Schemaänderungen ausrollt, spürt das im Alltag stärker als jeden Query-Benchmark.&lt;/p&gt;

&lt;p&gt;Zwei weitere Betriebsnotizen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Erweiterungen.&lt;/strong&gt; Postgres erlaubt PostGIS für Geodaten, pgvector für Embeddings, TimescaleDB für Zeitreihen — in derselben Datenbank, mit denselben Backups und denselben Transaktionen. MySQL hat keinen vergleichbaren Mechanismus. Falls Sie womöglich &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/data/brauchst-du-eine-vektordatenbank-postgres-vs-dedizierte-2026" rel="noopener noreferrer"&gt;eine Vektordatenbank&lt;/a&gt; brauchen, zählt das.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replikation.&lt;/strong&gt; MySQLs Binlog-Replikation ist alt, gut verstanden und hervorragend mit Werkzeugen versorgt. Postgres bietet physische Streaming-Replikation und logische Replikation; beides funktioniert gut, und die logische Replikation hat in jüngeren Releases deutlich zugelegt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wo SQLite hingehört
&lt;/h2&gt;

&lt;p&gt;SQLite landet oft in diesem Vergleich, meist mit falschem Rahmen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite ist keine kleine Serverdatenbank, sondern eine Bibliothek in Ihrem Prozess.&lt;/strong&gt; Kein Server, kein Port, keine Benutzerkonten, kein Netzwerk-Roundtrip — ein Funktionsaufruf liest eine Datei.&lt;/p&gt;

&lt;p&gt;Das macht es hervorragend für:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lokalen Anwendungszustand, Caches und Konfiguration&lt;/li&gt;
&lt;li&gt;Testsuites, wo eine Datenbank pro Test Mikrosekunden kostet&lt;/li&gt;
&lt;li&gt;Edge- und eingebettete Deployments&lt;/li&gt;
&lt;li&gt;leseintensive Lasten auf einer einzelnen Maschine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Und ungeeignet, sobald &lt;strong&gt;mehrere Anwendungsserver gleichzeitig schreiben&lt;/strong&gt; müssen. SQLite serialisiert Schreiber: einer nach dem anderen.&lt;/p&gt;

&lt;p&gt;Die ehrliche Regel: Fasst genau ein Prozess die Daten an, ist SQLite wahrscheinlich richtig — und die einfachste Antwort. Könnten es zwei sein, wollen Sie einen Server.&lt;/p&gt;

&lt;h2&gt;
  
  
  MariaDB gegen MySQL
&lt;/h2&gt;

&lt;p&gt;MariaDB entstand nach Oracles Übernahme als MySQL-Fork. Jahre später ist „Drop-in-Ersatz” nicht mehr zutreffend.&lt;/p&gt;

&lt;p&gt;Beide haben sich &lt;strong&gt;auseinanderentwickelt&lt;/strong&gt; : getrennte Optimizer-Arbeit, unterschiedliche Storage-Engines, und auf jeder Seite Funktionen, die der anderen fehlen. MariaDB hat &lt;code&gt;RETURNING&lt;/code&gt;, MySQL nicht. Die Versionsnummern korrespondieren überhaupt nicht mehr.&lt;/p&gt;

&lt;p&gt;Treiber und einfache Dumps sind weitgehend austauschbar. &lt;strong&gt;Replikation zwischen beiden und neuere Syntax sind es nicht.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MariaDB&lt;/strong&gt; für Community-Governance und den konkreten Funktionsumfang. &lt;strong&gt;MySQL&lt;/strong&gt; , wenn Sie an Oracles Ökosystem, einem darauf aufbauenden verwalteten Angebot oder der exakten Replikationssemantik hängen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Welche Versionen betreiben
&lt;/h2&gt;

&lt;p&gt;Stand September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Datenbank&lt;/th&gt;
&lt;th&gt;Aktuell&lt;/th&gt;
&lt;th&gt;Anmerkungen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;18&lt;/strong&gt; (18.6)&lt;/td&gt;
&lt;td&gt;19 in der Beta; &lt;strong&gt;14 endet im Nov. 2026&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;9.7 LTS&lt;/strong&gt; (9.7.3)&lt;/td&gt;
&lt;td&gt;8.4 LTS bis 2029; &lt;strong&gt;8.0 endete im Apr. 2026&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MariaDB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;12.3 LTS&lt;/strong&gt; (12.3.3)&lt;/td&gt;
&lt;td&gt;11.8 und 11.4 LTS weiterhin unterstützt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zwei Handlungspunkte: Wer auf &lt;strong&gt;MySQL 8.0&lt;/strong&gt; ist, läuft ohne Support — planen Sie den Wechsel auf 8.4 oder 9.7. Wer auf &lt;strong&gt;Postgres 14&lt;/strong&gt; ist, hat Zeit bis November.&lt;/p&gt;

&lt;p&gt;Postgres liefert jährlich eine Hauptversion mit fünf Jahren Support. MySQL teilt sich in quartalsweise Innovation-Releases und etwa zweijährliche LTS-Releases; betreiben Sie LTS, sofern Sie kein konkretes neues Feature brauchen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Entscheidungstabelle
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ihre Situation&lt;/th&gt;
&lt;th&gt;Wahl&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Komplexe Abfragen, Reporting neben OLTP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Postgres&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geodaten, Vektoren, Zeitreihen in einer Datenbank&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — Erweiterungen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Häufige Schema-Migrationen&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; — transaktionales DDL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tausende Verbindungen, kein Pooler möglich&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MySQL&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key-Value-artiger Zugriff über Primärschlüssel&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — geclusterter Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starke Leseskalierung über Replicas&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — ausgereifte Binlog-Werkzeuge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ein Prozess besitzt die Daten, kein Netzwerk&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SQLite&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Community-Governance statt Oracle&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MariaDB&lt;/strong&gt; oder Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team beherrscht eine davon tiefgehend&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Diese&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Das Fazit
&lt;/h2&gt;

&lt;p&gt;Die Feature-Lücke, die diese Debatte vor einem Jahrzehnt interessant machte, ist weitgehend geschlossen. Geblieben ist Strukturelles: &lt;strong&gt;geclusterte gegen Heap-Speicherung&lt;/strong&gt; , &lt;strong&gt;alte Zeilenversionen in der Tabelle gegen Undo-Logs&lt;/strong&gt; , &lt;strong&gt;ein Prozess gegen ein Thread pro Verbindung&lt;/strong&gt; , und &lt;strong&gt;ob eine gescheiterte Migration zurückrollbar ist&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Diese vier Unterschiede prägen Ihren Betriebsalltag weit stärker als jede Feature-Checkliste. Entscheiden Sie danach — und danach, welche Ihr Team um drei Uhr nachts debuggen kann.&lt;/p&gt;

&lt;p&gt;Und wenn Sie sich anhand eines Benchmarks entscheiden, den Sie nicht selbst gefahren haben, entscheiden Sie anhand der Last von jemand anderem.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ursprünglich veröffentlicht auf &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/data/postgres-vs-mysql-2026-vergleich" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgresvsmysql</category>
      <category>postgres</category>
      <category>mysql</category>
      <category>datenbankvergleich</category>
    </item>
    <item>
      <title>npm Supply Chain Attack Mitigation: What Actually Works</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/npm-supply-chain-attack-mitigation-what-actually-works-3e6c</link>
      <guid>https://dev.to/aleksei_aleinikov/npm-supply-chain-attack-mitigation-what-actually-works-3e6c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" alt="npm Supply Chain Attack Mitigation: What Actually Works" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you last read about npm supply chain security after the Shai-Hulud worm, your mental model is a year out of date. A lot shipped during 2026, and some of the advice everyone was giving is now done for you by default.&lt;/p&gt;

&lt;p&gt;This is what is actually true today: what you get for free, what is still opt-in, and what nobody is going to fix on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" alt="npm supply chain attack mitigation: what is default, what is opt-in, and what is still on you." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack this all responds to
&lt;/h2&gt;

&lt;p&gt;GitHub was notified on &lt;strong&gt;September 14, 2025&lt;/strong&gt; of the &lt;strong&gt;Shai-Hulud&lt;/strong&gt; attack. Worth restating because every control below is shaped by it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It got in through &lt;strong&gt;compromised maintainer accounts&lt;/strong&gt; , not a vulnerability in any package.&lt;/li&gt;
&lt;li&gt;It injected malicious &lt;strong&gt;post-install scripts&lt;/strong&gt; into popular packages.&lt;/li&gt;
&lt;li&gt;It was &lt;strong&gt;self-replicating&lt;/strong&gt; — a worm.&lt;/li&gt;
&lt;li&gt;It stole &lt;strong&gt;multiple kinds of secrets&lt;/strong&gt; , not just npm tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub removed &lt;strong&gt;500+ compromised packages&lt;/strong&gt; and blocked uploads matching the malware’s indicators of compromise.&lt;/p&gt;

&lt;p&gt;Every package involved was one you meant to install, from a maintainer you had reason to trust, at an unremarkable version number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your scanner said the tree was clean
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; compares your dependency tree against an &lt;strong&gt;advisory database&lt;/strong&gt;. For a finding to exist, somebody must first discover, report and publish it.&lt;/p&gt;

&lt;p&gt;A malicious version pushed twenty minutes ago has been through none of that. No advisory, no match, clean report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest update:&lt;/strong&gt; in August 2026 GitHub wired OpenSSF’s malicious-packages data into the Advisory Database, so malware advisories now sit alongside CVEs. The gap is narrower than it was.&lt;/p&gt;

&lt;p&gt;It is not closed. An advisory still trails the attack, and spreading inside that lag is the entire business model. Keep running &lt;code&gt;npm audit&lt;/code&gt;. Just stop treating a clean result as evidence you are fine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7x4m2n4bi8l7yb8w0qxc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7x4m2n4bi8l7yb8w0qxc.webp" alt="Why npm audit reports a clean tree: the advisory gap that this attack class lives inside." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you now get by default
&lt;/h2&gt;

&lt;p&gt;This is the part that changed, and the reason most 2025-era advice needs rereading.&lt;/p&gt;

&lt;h3&gt;
  
  
  npm v12 disables install scripts
&lt;/h3&gt;

&lt;p&gt;Announced June 2026 as a breaking change: &lt;strong&gt;npm v12 disables install scripts by default&lt;/strong&gt;. Attackers used them to exfiltrate credentials at install time rather than waiting for your code to import anything. You re-enable them by approving specific scripts.&lt;/p&gt;

&lt;p&gt;The same change &lt;strong&gt;blocks dependencies specified via git or remote URLs&lt;/strong&gt; by default, closing another install-time execution path.&lt;/p&gt;

&lt;p&gt;This is the single most important line in this article. The vector Shai-Hulud actually used is off by default — &lt;strong&gt;if you are on v12&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you are not, it is still your job:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Or in &lt;code&gt;.npmrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;ignore-scripts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;The honest cost:&lt;/strong&gt; packages with native components need their build step. A blanket ban across a large monorepo will break installs. Deny by default and allowlist the exceptions — the model pnpm has used for a while via &lt;code&gt;allowBuilds&lt;/code&gt;, with &lt;code&gt;dangerouslyAllowAllBuilds&lt;/code&gt; if you want out entirely. The name tells you what the maintainers think of that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependabot waits three days
&lt;/h3&gt;

&lt;p&gt;Since July 2026, Dependabot version updates &lt;strong&gt;wait until a release is at least three days old&lt;/strong&gt; before opening a pull request. Detection signals get time to surface first. &lt;strong&gt;On by default&lt;/strong&gt; , and &lt;strong&gt;security updates are exempt&lt;/strong&gt; — critical fixes are not delayed.&lt;/p&gt;

&lt;p&gt;Attackers depend on speed. This removes it, at almost no cost to you.&lt;/p&gt;

&lt;p&gt;pnpm enforces a stricter version at install time:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;pnpm-workspace.yaml&lt;/em&gt;&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;minimumReleaseAge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10080&lt;/span&gt; &lt;span class="c1"&gt;# minutes — one week&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;A version younger than that is not eligible for installation at all. &lt;code&gt;minimumReleaseAgeExclude&lt;/code&gt; handles the packages you want immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classic tokens are gone
&lt;/h3&gt;

&lt;p&gt;npm’s docs are unambiguous: &lt;strong&gt;as of November 2025, only granular access tokens are supported. Legacy access tokens have been removed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Granular tokens scope by package, scope and organisation, restrict by IP range, and take an expiry at least one day out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One footgun worth knowing:&lt;/strong&gt; granular tokens have a &lt;strong&gt;Bypass 2FA&lt;/strong&gt; option. It defaults to false, but when enabled it &lt;strong&gt;takes precedence over both account-level and package-level 2FA&lt;/strong&gt; for publishing. If you enforce 2FA on a package and someone has a bypass token, your enforcement is not what you think it is.&lt;/p&gt;

&lt;p&gt;Since &lt;strong&gt;August 2026&lt;/strong&gt; , bypass-2FA tokens can no longer perform account-identity or governance actions — changing email or password, touching 2FA config, managing tokens, adding maintainers. Those always require an interactive challenge. They can still publish directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compromised accounts get a cooling-off period
&lt;/h3&gt;

&lt;p&gt;Since June 2026, high-impact npm accounts drop into &lt;strong&gt;read-only mode for 72 hours&lt;/strong&gt; when the email changes or a 2FA recovery code is used. Phishing is how these campaigns start; this buys the real maintainer time to recover before the account can publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still opt-in
&lt;/h2&gt;

&lt;p&gt;Defaults handle the last attack. These are what you turn on yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trusted publishing
&lt;/h3&gt;

&lt;p&gt;OIDC between npm and your CI provider. A named workflow is authorised to publish. Each publish mints a short-lived, signed credential that &lt;strong&gt;cannot be extracted from logs or reused&lt;/strong&gt;.&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# required for OIDC&lt;/span&gt;

  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v6&lt;/span&gt;

    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

      &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;24'&lt;/span&gt;

      &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&lt;/span&gt;

      &lt;span class="na"&gt;package-manager-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# never cache in release builds&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Constraints to plan around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm CLI &lt;strong&gt;11.5.1+&lt;/strong&gt; , Node &lt;strong&gt;22.14.0+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions, GitLab CI/CD, and &lt;strong&gt;CircleCI since April 2026&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud-hosted runners only&lt;/strong&gt; — self-hosted is not supported&lt;/li&gt;
&lt;li&gt;Up to &lt;strong&gt;10 trusted publishers&lt;/strong&gt; per package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same idea as &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/security/kill-service-account-keys-workload-identity-federation-2026" rel="noopener noreferrer"&gt;killing service account keys with Workload Identity Federation&lt;/a&gt;: stop storing a long-lived credential, mint a short-lived one per run against a verified identity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjz9kzm3ml05dgcqmsox4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjz9kzm3ml05dgcqmsox4.webp" alt="The npm publish path with a long-lived token versus trusted publishing with OIDC." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Publishing this way from GitHub Actions or GitLab CI/CD generates &lt;strong&gt;provenance attestations automatically&lt;/strong&gt; — no &lt;code&gt;--provenance&lt;/code&gt; flag. Not supported for CircleCI, and not generated for packages published from &lt;strong&gt;private repositories&lt;/strong&gt; , even when the package is public.&lt;/p&gt;

&lt;h3&gt;
  
  
  Staged publishing
&lt;/h3&gt;

&lt;p&gt;Released May 2026, opt-in. Credentials alone no longer distribute a package: the version is &lt;strong&gt;staged until a maintainer approves it with 2FA&lt;/strong&gt; , in the CLI or on npmjs.com.&lt;/p&gt;

&lt;p&gt;This is the control that survives a fully compromised pipeline. An attacker with your CI credentials gets a staged version and a notification to a human, not a release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Close the doors you stopped using
&lt;/h3&gt;

&lt;p&gt;Setting up trusted publishing while the old automation token stays valid gains you little. Once the new flow works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Package Settings → Publishing access → &lt;strong&gt;“Require two-factor authentication and disallow tokens”&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Revoke the automation tokens you no longer need&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And when configuring 2FA, npm’s guidance is explicit: &lt;strong&gt;WebAuthn, not TOTP&lt;/strong&gt;. TOTP codes are phishable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch what leaves your runner
&lt;/h3&gt;

&lt;p&gt;GitHub’s &lt;strong&gt;Actions network firewall&lt;/strong&gt; is in technical preview and logs all outbound traffic from workflow runs, so you can spot a job suddenly talking to a domain it has never talked to before.&lt;/p&gt;

&lt;p&gt;This is the only item here that is &lt;strong&gt;detection&lt;/strong&gt; rather than prevention, which makes it disproportionately valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of this fixes
&lt;/h2&gt;

&lt;p&gt;The section the vendor posts skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A compromised maintainer can still publish malicious code.&lt;/strong&gt; Trusted publishing proves the package came from the right workflow in the right repository. It says nothing about whether that repository’s code is benign. Given commit access, provenance will faithfully attest to a malicious build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install scripts are not the only execution path.&lt;/strong&gt; npm v12 closing them raises the cost of the current pattern. It does nothing if the payload is in the library you import and run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cooldowns are a trade, not a win.&lt;/strong&gt; You accept a window running a version with a published vulnerability, to avoid being first to run a malicious one. Usually correct. Still a trade — which is exactly why Dependabot exempts security updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Almost none of this is detection.&lt;/strong&gt; Every control above lowers the odds of executing malicious code. Only egress monitoring tells you that you already did. Assume the answer might be yes, and know how fast you can rotate every secret a build job can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Move to npm v12 (install scripts off by default)&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Default once upgraded&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependabot cooldown&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Already on&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-only tokens for installs; no publish token in build jobs&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;On you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trusted publishing + disallow tokens&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Opt-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staged publishing&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Opt-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;minimumReleaseAge&lt;/code&gt; (pnpm)&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Opt-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit granular tokens for the Bypass 2FA flag&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;On you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebAuthn instead of TOTP&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;On you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress monitoring on CI runners&lt;/td&gt;
&lt;td&gt;Days&lt;/td&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things this week: &lt;strong&gt;upgrade to npm v12, or set &lt;code&gt;ignore-scripts&lt;/code&gt; until you can&lt;/strong&gt; , and &lt;strong&gt;get publish tokens out of every job that also installs dependencies&lt;/strong&gt;. That covers the path Shai-Hulud actually used.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The 2025 advice was “do these things yourself because nobody else will”. Through 2026 the ecosystem made the two highest-impact ones — no install scripts, no instant updates — the default.&lt;/p&gt;

&lt;p&gt;That is real progress, and it also means the remaining risk has moved. It now sits in the things still switched off: trusted publishing, staged publishing, and any idea of what your build machines talk to.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; will keep telling you the tree is clean. It is answering a narrower question than the one you should be asking.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/security/npm-supply-chain-attack-mitigation-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>npmsupplychainattack</category>
      <category>supplychainsecurity</category>
      <category>trustedpublishing</category>
      <category>npmprovenance</category>
    </item>
    <item>
      <title>npm-Lieferkettenangriffe abwehren: Was wirklich hilft</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/npm-lieferkettenangriffe-abwehren-was-wirklich-hilft-1mf2</link>
      <guid>https://dev.to/aleksei_aleinikov/npm-lieferkettenangriffe-abwehren-was-wirklich-hilft-1mf2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" alt="npm-Lieferkettenangriffe abwehren: Was wirklich hilft" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wenn Sie zuletzt nach dem Shai-Hulud-Wurm über npm-Lieferkettensicherheit gelesen haben, ist Ihr mentales Modell ein Jahr alt. 2026 kam vieles dazu, und ein Teil der damals überall empfohlenen Maßnahmen erledigt npm inzwischen selbst.&lt;/p&gt;

&lt;p&gt;Hier steht, was heute tatsächlich gilt: was Sie geschenkt bekommen, was weiterhin optional ist, und was niemand für Sie löst.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feqkofo11fv1vo3v9fxn8.webp" alt="npm-Lieferkettenangriffe abwehren: was Standard ist, was optional, und was bei Ihnen bleibt." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Der Angriff, auf den das alles antwortet
&lt;/h2&gt;

&lt;p&gt;GitHub wurde am &lt;strong&gt;14. September 2025&lt;/strong&gt; über den &lt;strong&gt;Shai-Hulud&lt;/strong&gt; -Angriff informiert. Die Wiederholung lohnt, weil jede Maßnahme unten von ihm geprägt ist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Einstieg über &lt;strong&gt;kompromittierte Maintainer-Konten&lt;/strong&gt; , nicht über eine Paketschwachstelle.&lt;/li&gt;
&lt;li&gt;Eingeschleust wurden bösartige &lt;strong&gt;Post-Install-Skripte&lt;/strong&gt; in populäre Pakete.&lt;/li&gt;
&lt;li&gt;Der Code war &lt;strong&gt;selbstreplizierend&lt;/strong&gt; — ein Wurm.&lt;/li&gt;
&lt;li&gt;Gestohlen wurden &lt;strong&gt;mehrere Arten von Geheimnissen&lt;/strong&gt; , nicht nur npm-Tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub entfernte &lt;strong&gt;über 500 kompromittierte Pakete&lt;/strong&gt; und blockierte Uploads, die den Indicators of Compromise entsprachen.&lt;/p&gt;

&lt;p&gt;Jedes beteiligte Paket war eines, das Sie installieren wollten, von einem Maintainer mit gutem Ruf, in einer unauffälligen Versionsnummer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum Ihr Scanner „sauber” meldete
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; gleicht Ihren Abhängigkeitsbaum mit einer &lt;strong&gt;Advisory-Datenbank&lt;/strong&gt; ab. Damit ein Befund existiert, muss ihn zuerst jemand entdecken, melden und veröffentlichen.&lt;/p&gt;

&lt;p&gt;Eine vor zwanzig Minuten publizierte Schadversion hat davon nichts durchlaufen. Kein Advisory, keine Übereinstimmung, sauberer Bericht.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eine ehrliche Aktualisierung:&lt;/strong&gt; Im August 2026 hat GitHub die Malicious-Packages-Daten der OpenSSF in die Advisory-Datenbank eingebunden. Malware-Advisories stehen jetzt neben CVEs, die Lücke ist kleiner als zuvor.&lt;/p&gt;

&lt;p&gt;Geschlossen ist sie nicht. Ein Advisory hinkt dem Angriff weiterhin hinterher, und sich in dieser Verzögerung auszubreiten ist das gesamte Geschäftsmodell. Lassen Sie &lt;code&gt;npm audit&lt;/code&gt; laufen — hören Sie nur auf, ein sauberes Ergebnis als Beweis Ihrer Unversehrtheit zu lesen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7x4m2n4bi8l7yb8w0qxc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7x4m2n4bi8l7yb8w0qxc.webp" alt="Warum npm audit einen sauberen Baum meldet: die Advisory-Lücke, in der diese Angriffsklasse lebt." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Was Sie jetzt standardmäßig bekommen
&lt;/h2&gt;

&lt;p&gt;Das ist der Teil, der sich geändert hat — und der Grund, warum die meisten Ratschläge aus 2025 neu gelesen werden müssen.&lt;/p&gt;

&lt;h3&gt;
  
  
  npm v12 deaktiviert Install-Skripte
&lt;/h3&gt;

&lt;p&gt;Im Juni 2026 als Breaking Change angekündigt: &lt;strong&gt;npm v12 deaktiviert Install-Skripte standardmäßig&lt;/strong&gt;. Angreifer nutzten sie, um Zugangsdaten schon bei der Installation abzuziehen, statt darauf zu warten, dass Ihr Code irgendetwas importiert. Reaktivieren lässt sich das durch gezielte Freigabe einzelner Skripte.&lt;/p&gt;

&lt;p&gt;Dieselbe Änderung &lt;strong&gt;blockiert Abhängigkeiten über Git- oder Remote-URLs&lt;/strong&gt; und schließt damit einen weiteren Ausführungspfad zur Installationszeit.&lt;/p&gt;

&lt;p&gt;Das ist die wichtigste Zeile dieses Artikels. Genau der Vektor, den Shai-Hulud nutzte, ist standardmäßig aus — &lt;strong&gt;sofern Sie auf v12 sind&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Falls nicht, bleibt es Ihre Aufgabe:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal window&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Oder in &lt;code&gt;.npmrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ignore-scripts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Die ehrlichen Kosten:&lt;/strong&gt; Pakete mit nativen Bestandteilen brauchen ihren Build-Schritt. Ein pauschales Verbot in einem großen Monorepo bricht Installationen. Standardmäßig verbieten, Ausnahmen freigeben — dieses Modell fährt pnpm seit Längerem über &lt;code&gt;allowBuilds&lt;/code&gt;, mit &lt;code&gt;dangerouslyAllowAllBuilds&lt;/code&gt; für den kompletten Ausstieg. Der Name verrät, was die Maintainer davon halten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependabot wartet drei Tage
&lt;/h3&gt;

&lt;p&gt;Seit Juli 2026 warten Dependabot-Versionsupdates, &lt;strong&gt;bis ein Release mindestens drei Tage alt ist&lt;/strong&gt; , bevor ein Pull Request geöffnet wird. Erkennungssignale bekommen so einen Vorsprung. &lt;strong&gt;Standardmäßig aktiv&lt;/strong&gt; , und &lt;strong&gt;Sicherheitsupdates sind ausgenommen&lt;/strong&gt; — kritische Fixes verzögern sich nicht.&lt;/p&gt;

&lt;p&gt;Angreifer leben von Geschwindigkeit. Das nimmt sie ihnen, und kostet Sie praktisch nichts.&lt;/p&gt;

&lt;p&gt;pnpm setzt eine strengere Variante schon bei der Installation durch:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;pnpm-workspace.yaml&lt;/em&gt;&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;minimumReleaseAge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10080&lt;/span&gt; &lt;span class="c1"&gt;# Minuten — eine Woche&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Eine jüngere Version kommt gar nicht erst infrage. &lt;code&gt;minimumReleaseAgeExclude&lt;/code&gt; deckt die Pakete ab, die Sie sofort wollen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Klassische Tokens gibt es nicht mehr
&lt;/h3&gt;

&lt;p&gt;Die npm-Dokumentation ist eindeutig: &lt;strong&gt;Seit November 2025 werden nur noch granulare Zugriffstokens unterstützt, Legacy-Tokens wurden entfernt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Granulare Tokens lassen sich nach Paket, Scope und Organisation eingrenzen, auf IP-Bereiche beschränken und mit einem Ablaufdatum von mindestens einem Tag versehen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ein Fallstrick, den man kennen sollte:&lt;/strong&gt; Granulare Tokens haben eine Option &lt;strong&gt;Bypass 2FA&lt;/strong&gt;. Sie ist standardmäßig aus, hebt aber im aktivierten Zustand &lt;strong&gt;sowohl konto- als auch paketweite 2FA&lt;/strong&gt; beim Veröffentlichen aus. Wenn Sie 2FA für ein Paket erzwingen und jemand ein Bypass-Token besitzt, ist Ihre Durchsetzung nicht das, wofür Sie sie halten.&lt;/p&gt;

&lt;p&gt;Seit &lt;strong&gt;August 2026&lt;/strong&gt; können Bypass-2FA-Tokens keine Konto- oder Governance-Aktionen mehr ausführen — E-Mail oder Passwort ändern, 2FA-Konfiguration anfassen, Tokens verwalten, Maintainer hinzufügen. Dafür ist immer eine interaktive Challenge nötig. Direkt veröffentlichen dürfen sie weiterhin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kompromittierte Konten bekommen eine Abkühlphase
&lt;/h3&gt;

&lt;p&gt;Seit Juni 2026 gehen npm-Konten mit hoher Reichweite für &lt;strong&gt;72 Stunden in einen Nur-Lese-Modus&lt;/strong&gt; , wenn die E-Mail geändert oder ein 2FA-Wiederherstellungscode benutzt wird. Phishing steht am Anfang dieser Kampagnen; das verschafft dem echten Maintainer Zeit zur Rückgewinnung, bevor das Konto veröffentlichen kann.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was weiterhin optional ist
&lt;/h2&gt;

&lt;p&gt;Standardeinstellungen decken den letzten Angriff ab. Das Folgende schalten Sie selbst ein.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trusted Publishing
&lt;/h3&gt;

&lt;p&gt;OIDC zwischen npm und Ihrem CI-Anbieter. Ein benannter Workflow ist zum Veröffentlichen autorisiert. Jede Veröffentlichung erzeugt ein kurzlebiges, signiertes Credential, das sich &lt;strong&gt;weder aus Logs extrahieren noch wiederverwenden lässt&lt;/strong&gt;.&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# für OIDC erforderlich&lt;/span&gt;

  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v6&lt;/span&gt;

    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

      &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;24'&lt;/span&gt;

      &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&lt;/span&gt;

      &lt;span class="na"&gt;package-manager-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# in Release-Builds nie cachen&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish&lt;/span&gt;

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

&lt;/div&gt;





&lt;p&gt;Randbedingungen für die Planung:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm CLI &lt;strong&gt;11.5.1+&lt;/strong&gt; , Node &lt;strong&gt;22.14.0+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions, GitLab CI/CD und &lt;strong&gt;CircleCI seit April 2026&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nur cloud-gehostete Runner&lt;/strong&gt; — selbst gehostete werden nicht unterstützt&lt;/li&gt;
&lt;li&gt;Bis zu &lt;strong&gt;10 Trusted Publisher&lt;/strong&gt; pro Paket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dieselbe Idee wie beim &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/security/service-account-schluessel-abschaffen-workload-identity-federation-2026" rel="noopener noreferrer"&gt;Abschaffen von Service-Account-Schlüsseln mit Workload Identity Federation&lt;/a&gt;: kein langlebiges Credential speichern, sondern pro Lauf ein kurzlebiges gegen eine verifizierte Identität ausstellen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjz9kzm3ml05dgcqmsox4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjz9kzm3ml05dgcqmsox4.webp" alt="Der npm-Veröffentlichungspfad mit langlebigem Token gegenüber Trusted Publishing mit OIDC." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wer so aus GitHub Actions oder GitLab CI/CD veröffentlicht, erhält &lt;strong&gt;Provenance-Attestierungen automatisch&lt;/strong&gt; — ohne &lt;code&gt;--provenance&lt;/code&gt;. Für CircleCI nicht unterstützt, und für Pakete aus &lt;strong&gt;privaten Repositories&lt;/strong&gt; wird keine erzeugt, selbst wenn das Paket öffentlich ist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Staged Publishing
&lt;/h3&gt;

&lt;p&gt;Mai 2026, optional. Credentials allein verteilen kein Paket mehr: Die Version bleibt &lt;strong&gt;im Staging, bis ein Maintainer sie mit 2FA freigibt&lt;/strong&gt; , in der CLI oder auf npmjs.com.&lt;/p&gt;

&lt;p&gt;Das ist die Maßnahme, die eine vollständig kompromittierte Pipeline überlebt. Ein Angreifer mit Ihren CI-Credentials bekommt eine gestagte Version und eine Benachrichtigung an einen Menschen — kein Release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Die nicht mehr genutzten Türen schließen
&lt;/h3&gt;

&lt;p&gt;Trusted Publishing einzurichten und den alten Automation-Token gültig zu lassen, bringt wenig. Sobald der neue Ablauf läuft:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Package Settings → Publishing access → &lt;strong&gt;„Require two-factor authentication and disallow tokens”&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Nicht mehr benötigte Automation-Tokens widerrufen&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Und bei der 2FA-Einrichtung ist npms Empfehlung eindeutig: &lt;strong&gt;WebAuthn statt TOTP&lt;/strong&gt;. TOTP-Codes sind phishbar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beobachten, was den Runner verlässt
&lt;/h3&gt;

&lt;p&gt;GitHubs &lt;strong&gt;Actions Network Firewall&lt;/strong&gt; ist in der Technical Preview und protokolliert den gesamten ausgehenden Verkehr von Workflow-Läufen — Sie sehen also, wenn ein Job plötzlich mit einer Domain spricht, mit der er nie zuvor gesprochen hat.&lt;/p&gt;

&lt;p&gt;Das ist der einzige Punkt hier, der &lt;strong&gt;Erkennung&lt;/strong&gt; statt Prävention leistet, und deshalb überproportional wertvoll.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was all das nicht löst
&lt;/h2&gt;

&lt;p&gt;Der Abschnitt, den Hersteller-Posts weglassen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ein kompromittierter Maintainer kann weiterhin Schadcode veröffentlichen.&lt;/strong&gt; Trusted Publishing belegt, dass das Paket aus dem richtigen Workflow im richtigen Repository kam. Über die Gutartigkeit des Codes sagt es nichts. Mit Commit-Zugriff attestiert die Provenance getreulich einen bösartigen Build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install-Skripte sind nicht der einzige Ausführungspfad.&lt;/strong&gt; Dass npm v12 sie schließt, verteuert das aktuelle Muster. Gegen eine Nutzlast in der Bibliothek, die Sie importieren und ausführen, hilft es nicht.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cooldowns sind ein Kompromiss, kein Gewinn.&lt;/strong&gt; Sie akzeptieren ein Fenster mit einer bekannt verwundbaren Version, um nicht die Ersten mit einer bösartigen zu sein. Meist richtig. Ein Kompromiss bleibt es — genau deshalb nimmt Dependabot Sicherheitsupdates aus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast nichts davon ist Erkennung.&lt;/strong&gt; Jede Maßnahme senkt die Wahrscheinlichkeit, Schadcode auszuführen. Nur Egress-Monitoring sagt Ihnen, dass es bereits passiert ist. Gehen Sie davon aus, dass die Antwort ja lauten könnte — und wissen Sie, wie schnell Sie jedes Geheimnis rotieren können, das ein Build-Job sieht.&lt;/p&gt;

&lt;h2&gt;
  
  
  Womit anfangen
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Maßnahme&lt;/th&gt;
&lt;th&gt;Aufwand&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auf npm v12 wechseln (Install-Skripte aus)&lt;/td&gt;
&lt;td&gt;Stunden&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nach Upgrade Standard&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependabot-Cooldown&lt;/td&gt;
&lt;td&gt;Keiner&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Bereits aktiv&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nur-Lese-Tokens fürs Installieren; kein Publish-Token im Build-Job&lt;/td&gt;
&lt;td&gt;Stunden&lt;/td&gt;
&lt;td&gt;Bei Ihnen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trusted Publishing + Tokens verbieten&lt;/td&gt;
&lt;td&gt;Stunden&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staged Publishing&lt;/td&gt;
&lt;td&gt;Stunden&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;minimumReleaseAge&lt;/code&gt; (pnpm)&lt;/td&gt;
&lt;td&gt;Stunden&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Granulare Tokens auf Bypass-2FA prüfen&lt;/td&gt;
&lt;td&gt;Minuten&lt;/td&gt;
&lt;td&gt;Bei Ihnen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebAuthn statt TOTP&lt;/td&gt;
&lt;td&gt;Minuten&lt;/td&gt;
&lt;td&gt;Bei Ihnen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress-Monitoring auf CI-Runnern&lt;/td&gt;
&lt;td&gt;Tage&lt;/td&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zwei Dinge diese Woche: &lt;strong&gt;auf npm v12 wechseln oder bis dahin &lt;code&gt;ignore-scripts&lt;/code&gt; setzen&lt;/strong&gt; , und &lt;strong&gt;Publish-Tokens aus jedem Job entfernen, der auch Abhängigkeiten installiert&lt;/strong&gt;. Damit ist der Weg abgedeckt, den Shai-Hulud tatsächlich genommen hat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Das Fazit
&lt;/h2&gt;

&lt;p&gt;Der Ratschlag von 2025 lautete: „Tun Sie das selbst, weil es sonst niemand tut.” Im Lauf von 2026 hat das Ökosystem die beiden wirksamsten Punkte — keine Install-Skripte, keine sofortigen Updates — zum Standard gemacht.&lt;/p&gt;

&lt;p&gt;Das ist echter Fortschritt. Es bedeutet aber auch, dass sich das verbleibende Risiko verschoben hat: Es sitzt jetzt in dem, was weiterhin ausgeschaltet ist — Trusted Publishing, Staged Publishing und jede Vorstellung davon, mit wem Ihre Build-Maschinen sprechen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; wird Ihnen weiterhin sagen, der Baum sei sauber. Es beantwortet eine engere Frage als die, die Sie stellen sollten.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ursprünglich veröffentlicht auf &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/security/npm-lieferkettenangriffe-abwehren-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>npmlieferkettenangri</category>
      <category>supplychainsecurity</category>
      <category>trustedpublishing</category>
      <category>npmprovenance</category>
    </item>
    <item>
      <title>Iceberg vs. Delta Lake 2026: Ein verständlicher Leitfaden zur Auswahl</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Tue, 08 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/iceberg-vs-delta-lake-2026-ein-verstandlicher-leitfaden-zur-auswahl-7le</link>
      <guid>https://dev.to/aleksei_aleinikov/iceberg-vs-delta-lake-2026-ein-verstandlicher-leitfaden-zur-auswahl-7le</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" alt="Iceberg vs. Delta Lake 2026: Ein verständlicher Leitfaden zur Auswahl" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Die meisten Artikel über Iceberg und Delta Lake beginnen mit einem Funktionsvergleich. Das ist der falsche Anfang — denn 2026 sehen die Funktionslisten fast identisch aus, und weil den meisten, die diese Frage stellen, nie erklärt wurde, was ein Tabellenformat überhaupt &lt;em&gt;ist&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Fangen wir also dort an, in einfacher Sprache, und vergleichen erst danach.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" alt="Iceberg vs. Delta Lake 2026: Was ein Tabellenformat ist und wie man eines auswählt." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Was ein Tabellenformat wirklich ist
&lt;/h2&gt;

&lt;p&gt;Sie haben einen Ordner im Objektspeicher. Darin liegen ein paar tausend Parquet-Dateien. Jemand nennt das „eine Tabelle“.&lt;/p&gt;

&lt;p&gt;Das ist keine Tabelle. Das ist ein Ordner. Und daraus folgen drei echte Probleme:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcamaoe9si6fchv949ep.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcamaoe9si6fchv949ep.webp" alt="Ein Ordner mit Parquet-Dateien gegenüber denselben Dateien mit einer Tabellenformat-Metadatenschicht darüber." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Niemand ist sich einig, was drin ist.&lt;/strong&gt; Eine Query-Engine muss den Ordner auflisten, um es herauszufinden. Schreibt ein Job gerade neue Dateien, sieht ein Leser sie und ein anderer nicht.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sie können nichts sicher ändern.&lt;/strong&gt; Eine Zeile zu löschen bedeutet, eine Datei neu zu schreiben. Tauschen Sie sie aus, während ein Leser mitten im Scan ist, sind die Ergebnisse falsch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sie können nicht zurück.&lt;/strong&gt; Überschreiben Sie die Datei von gestern, ist gestern weg.&lt;/p&gt;

&lt;p&gt;Ein &lt;strong&gt;Tabellenformat&lt;/strong&gt; ist ein Regelwerk, das genau das löst. Es ergänzt eine Metadatenschicht, die verfolgt, welche Dateien zur Tabelle gehören, wie das Schema aussieht und wie die Tabelle zu jedem Zeitpunkt aussah. Leser fragen die Metadaten, nicht den Ordner.&lt;/p&gt;

&lt;p&gt;Mehr ist es nicht. Apache Iceberg und Delta Lake sind zwei konkurrierende Regelwerke für dieselbe Aufgabe. Die Daten darunter sind in beiden Fällen weiterhin Parquet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Antwort in 30 Sekunden
&lt;/h2&gt;

&lt;p&gt;Falls Sie hier aufhören wollen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schon auf Databricks?&lt;/strong&gt; Delta Lake. Es ist das native Format, die Integration ist dort am tiefsten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximale Engine- und Anbieterunabhängigkeit?&lt;/strong&gt; Iceberg. Es hat die breiteste Streuung unabhängiger Engines und Kataloge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neuanfang ohne starke Bindung?&lt;/strong&gt; Iceberg ist 2026 die sicherere Voreinstellung, weil mehr unabhängige Engines es nativ sprechen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tief in Delta und besorgt wegen Lock-in?&lt;/strong&gt; Möglicherweise müssen Sie gar nicht wechseln — siehe UniForm weiter unten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alles Weitere ist die Begründung.&lt;/p&gt;

&lt;h2&gt;
  
  
  Woher sie kommen, und warum das noch zählt
&lt;/h2&gt;

&lt;p&gt;Dieser Teil erklärt die meisten Unterschiede.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iceberg&lt;/strong&gt; ist ein Projekt der Apache Software Foundation. ASF-Governance bedeutet, dass kein einzelner Anbieter die Spezifikation kontrolliert — und das zeigt sich im Ökosystem: BigQuery, Snowflake, Redshift, Athena, Trino, ClickHouse, DuckDB, Dremio, StarRocks, Doris, Druid, Firebolt und Microsoft OneLake lesen es alle, und die Katalogschicht hat mehrere unabhängige Implementierungen, darunter Apache Polaris, Apache Gravitino, AWS Glue, Nessie und Lakekeeper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delta Lake&lt;/strong&gt; entstand bei Databricks und wurde 2019 zu einem Projekt der Linux Foundation. Das Projekt schreibt unmissverständlich, es sei „ein unabhängiges Open-Source-Projekt und wird nicht von einem einzelnen Unternehmen kontrolliert“, und nennt über 190 Entwickler aus mehr als 70 Organisationen. Sein Schwerpunkt liegt aber weiterhin bei Databricks, und die aktivste jüngere Entwicklung — die Unity Catalog Delta APIs — steht Databricks nahe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was das praktisch bedeutet:&lt;/strong&gt; Wenn Ihre Sorge lautet „kann ich meine eigenen Daten in fünf Jahren mit einem Werkzeug lesen, das ich noch nicht gewählt habe“, ist Icebergs Governance die konservativere Wette. Ist Ihre Plattform ohnehin Databricks, ist diese Sorge weitgehend theoretisch und Delta bietet den besseren Alltag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was tatsächlich gleich ist
&lt;/h2&gt;

&lt;p&gt;Mehr, als das Marketing nahelegt. Beide bieten:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fähigkeit&lt;/th&gt;
&lt;th&gt;Was das heißt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACID-Transaktionen&lt;/td&gt;
&lt;td&gt;Schreiber zerstören sich nicht gegenseitig; Leser sehen einen konsistenten Snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Travel&lt;/td&gt;
&lt;td&gt;Die Tabelle abfragen, wie sie gestern aussah; einen fehlerhaften Ladelauf zurückrollen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema-Evolution&lt;/td&gt;
&lt;td&gt;Spalten hinzufügen, umbenennen, löschen und umsortieren, ohne Daten neu zu schreiben&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zeilenweise Löschungen und Updates&lt;/td&gt;
&lt;td&gt;Einzelne Zeilen ändern, ohne ganze Dateien neu zu schreiben&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming und Batch&lt;/td&gt;
&lt;td&gt;Dieselbe Tabelle bedient beides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parquet darunter&lt;/td&gt;
&lt;td&gt;Ihre eigentlichen Datendateien sind in beiden Fällen dieselben&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Wenn Ihnen jemand eines davon als Alleinstellungsmerkmal verkauft, verkauft er Ihnen etwas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was tatsächlich anders ist
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm31c747mnstlhh8bn0jr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm31c747mnstlhh8bn0jr.webp" alt="Was Iceberg und Delta Lake wirklich unterscheidet: Partitionierung, Governance und Interoperabilität." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drei Dinge unterscheiden sich wirklich.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Iceberg verbirgt die Partitionierung, Delta nicht.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Das ist Icebergs nützlichstes Alleinstellungsmerkmal und das, dessen Verständnis sich am meisten lohnt.&lt;/p&gt;

&lt;p&gt;In älteren Systemen musste eine Abfrage explizit auf die Partitionsspalte filtern, sonst wurde alles gescannt. Nutzer mussten das physische Layout kennen.&lt;/p&gt;

&lt;p&gt;Iceberg hinterlegt die &lt;em&gt;Transformation&lt;/em&gt; — „partitioniere nach Tag dieser Zeitstempelspalte“ — als Tabellenkonfiguration. Sie schreiben einen normalen Filter auf den Zeitstempel, und Iceberg leitet den Partitionsfilter ab und überspringt Dateien für Sie.&lt;/p&gt;

&lt;p&gt;Noch besser: Es unterstützt &lt;strong&gt;Partition Evolution&lt;/strong&gt;. Beginnen Sie mit Monatspartitionen, stellen Sie fest, dass die Daten gewachsen sind, wechseln Sie auf Tage — ohne die bestehenden Daten neu zu schreiben. Alte Dateien behalten ihr altes Schema, neue nutzen das neue, und Abfragen funktionieren weiter, weil Filter abgeleitet und nicht fest verdrahtet sind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Delta beantwortet Interoperabilität mit UniForm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deltas Antwort auf „aber alle anderen nutzen Iceberg“ ist das Delta Universal Format. UniForm erlaubt es, Delta-Tabellen mit Iceberg- und Hudi-Clients zu lesen.&lt;/p&gt;

&lt;p&gt;Das ist ein ausgesprochen pragmatischer Zug. Hat Ihre Organisation vor drei Jahren auf Delta standardisiert, brauchen Sie nicht zwingend ein Migrationsprojekt — womöglich genügt es, UniForm einzuschalten und Iceberg-sprechende Engines lesen zu lassen, was ohnehin da ist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Delta liefert einen Kernel, den Engines einbetten.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Delta pflegt einen Kernel, unter anderem in Rust, den andere Engines einbetten können, statt das Protokoll neu zu implementieren. ClickHouse hat den Rust Delta Kernel dieses Jahr integriert. Das senkt die Hürde für neue Engines, Delta korrekt zu unterstützen — wichtig, weil in Protokoll-Neuimplementierungen die subtilen Fehler wohnen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was tatsächlich entscheidet
&lt;/h2&gt;

&lt;p&gt;Hier kommt der Teil, den die meisten Vergleiche auslassen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sie werden Ihr Tabellenformat leichter wechseln als Ihren Katalog.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Der Katalog verfolgt, welche Tabellen existieren, wo deren Metadaten liegen und wer sie lesen darf. Iceberg hat genau dafür eine REST-Catalog-Spezifikation, um das zu entkoppeln, und es gibt mehrere Implementierungen. Deltas jüngste Arbeit dreht sich um die Unity Catalog Delta APIs.&lt;/p&gt;

&lt;p&gt;Ihr Katalog hängt an Ihrem Berechtigungsmodell, Ihrem Lineage-Tooling, Ihrer CI und jeder Pipeline, die Sie betreiben. Ein Tabellenformat zu migrieren ist ein dokumentiertes Verfahren — Iceberg veröffentlicht sogar einen Delta-Lake-Migrationsleitfaden. Einen Katalog zu migrieren ist ein Projekt.&lt;/p&gt;

&lt;p&gt;Das ehrliche Entscheidungsverfahren lautet also:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Welcher Katalog wird diese Daten verwalten?&lt;/strong&gt; Lautet die Antwort Unity Catalog, sind Sie bei Delta. Lautet sie Polaris, Glue, Gravitino, BigLake oder Nessie, sind Sie bei Iceberg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Welche Engines müssen lesen?&lt;/strong&gt; Listen Sie sie auf und prüfen Sie, welches Format jede nativ spricht — nicht über eine Brücke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erst dann&lt;/strong&gt; schauen Sie auf Funktionen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wer direkt zu Schritt 3 springt, trifft eine Entscheidung, die Schritt 1 sechs Monate später still überstimmt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Welche Spezifikationsversion
&lt;/h2&gt;

&lt;p&gt;Erwähnenswert, weil es regelmäßig für Verwirrung sorgt.&lt;/p&gt;

&lt;p&gt;Bei &lt;strong&gt;Iceberg&lt;/strong&gt; sind die Spezifikationsversionen 1, 2 und 3 vollständig und von der Community übernommen. Version 3 brachte erweiterte Typen (Nanosekunden-Zeitstempel, Variant, Geometry und Geography), Standardwerte für Spalten, Row-Lineage-Tracking und binäre Deletion Vectors. &lt;strong&gt;Version 4 befindet sich in aktiver Entwicklung und wurde nicht formal übernommen&lt;/strong&gt; — sie strukturiert Metadaten um und führt relative Pfade ein, damit Tabellen verschoben werden können, ohne Metadaten neu zu schreiben. Planen Sie noch nicht damit.&lt;/p&gt;

&lt;p&gt;Bei &lt;strong&gt;Delta Lake&lt;/strong&gt; ist die aktuelle Linie &lt;strong&gt;4.4.0 auf Apache Spark 4.2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Entscheidungstabelle
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ihre Situation&lt;/th&gt;
&lt;th&gt;Wahl&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Databricks ist Ihre Plattform&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Delta Lake&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mehrere Query-Engines, mehrere Anbieter&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Iceberg&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery oder Snowflake als primäres Warehouse&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — beide lesen es nativ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sie müssen die Partitionierung später ändern&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — Partition Evolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schon auf Delta, brauchen Iceberg-Leser&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bei Delta bleiben&lt;/strong&gt; , UniForm aktivieren&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance und Anbieterneutralität haben Priorität&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — ASF-Governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neuanfang, keine Randbedingungen&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — breitere native Unterstützung&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ihr Katalog ist Unity Catalog&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Delta Lake&lt;/strong&gt; — die Entscheidung ist bereits gefallen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Wenn Sie noch entscheiden, was &lt;em&gt;über&lt;/em&gt; dem Tabellenformat sitzt: Der Warehouse-Vergleich in &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/data/bigquery-vs-snowflake-2026-ehrlicher-vergleich" rel="noopener noreferrer"&gt;BigQuery vs. Snowflake&lt;/a&gt; behandelt die Schicht, die diese Tabellen konsumieren wird.&lt;/p&gt;

&lt;h2&gt;
  
  
  Das Fazit
&lt;/h2&gt;

&lt;p&gt;Der Formatkrieg ist leiser, als die Blogposts vermuten lassen. Beide Formate erledigen dieselbe Kernaufgabe, beide sind produktionsreif, und die Interoperabilitätsschichten sorgen dafür, dass eine „falsche“ Wahl heilbar bleibt.&lt;/p&gt;

&lt;p&gt;Nicht günstig heilbar sind dagegen eine Katalogentscheidung, eine Engine-Festlegung oder ein Berechtigungsmodell auf der falschen Annahme. Entscheiden Sie diese zuerst, dann entscheidet sich das Tabellenformat meist von selbst.&lt;/p&gt;

&lt;p&gt;Und wenn Sie eine technische Sache mitnehmen: &lt;strong&gt;Hidden Partitioning und Partition Evolution sind der echte Iceberg-Vorteil.&lt;/strong&gt; Nicht weil sie aufregend wären, sondern weil „wir haben das vor zwei Jahren falsch partitioniert und können es ohne Neuschreiben nicht korrigieren“ ein wirklich teurer Satz ist — und Iceberg ist dasjenige, das Ihnen erspart, ihn zu sagen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ursprünglich veröffentlicht auf &lt;a href="https://www.alekseialeinikov.com/de/blog/topics/data/iceberg-vs-delta-lake-2026-vergleich" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>apacheiceberg</category>
      <category>deltalake</category>
      <category>tabellenformat</category>
      <category>lakehouse</category>
    </item>
    <item>
      <title>Iceberg vs Delta Lake in 2026: A Plain Guide to Picking One</title>
      <dc:creator>Aleksei Aleinikov</dc:creator>
      <pubDate>Tue, 08 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/aleksei_aleinikov/iceberg-vs-delta-lake-in-2026-a-plain-guide-to-picking-one-hpa</link>
      <guid>https://dev.to/aleksei_aleinikov/iceberg-vs-delta-lake-in-2026-a-plain-guide-to-picking-one-hpa</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" alt="Iceberg vs Delta Lake in 2026: A Plain Guide to Picking One" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most articles about Iceberg and Delta Lake start by comparing features. That is the wrong place to start, because in 2026 the feature lists look almost identical, and because most people asking the question have not been told what a table format actually &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So let us start there, in plain language, and only then compare.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjvtbvk7xm01ik6nnn3r.webp" alt="Iceberg vs Delta Lake in 2026: what a table format is and how to choose one." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a table format actually is
&lt;/h2&gt;

&lt;p&gt;You have a folder in object storage. Inside are a few thousand Parquet files. Somebody calls this “a table”.&lt;/p&gt;

&lt;p&gt;It is not a table. It is a folder. And that causes three real problems:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcamaoe9si6fchv949ep.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcamaoe9si6fchv949ep.webp" alt="A folder of Parquet files versus the same files with a table format metadata layer on top." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody agrees what is in it.&lt;/strong&gt; A query engine has to list the folder to find out. If a job is halfway through writing new files, one reader sees them and another does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot safely change anything.&lt;/strong&gt; Deleting a row means rewriting a file. If a reader is mid-scan when you swap it, results are wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot go back.&lt;/strong&gt; Overwrite yesterday’s file and yesterday is gone.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;table format&lt;/strong&gt; is a rulebook that fixes this. It adds a metadata layer that tracks which files belong to the table, what the schema is, and what the table looked like at each point in time. Readers ask the metadata, not the folder.&lt;/p&gt;

&lt;p&gt;That is it. Apache Iceberg and Delta Lake are two competing rulebooks for the same job. The data underneath is still Parquet in both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second answer
&lt;/h2&gt;

&lt;p&gt;If you want to stop reading here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already on Databricks?&lt;/strong&gt; Delta Lake. It is the native format and the integration is deepest there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want maximum engine and vendor independence?&lt;/strong&gt; Iceberg. It has the broadest spread of independent engines and catalogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting fresh with no strong tie?&lt;/strong&gt; Iceberg is the safer default in 2026, because more independent engines speak it natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already deep in Delta and worried about lock-in?&lt;/strong&gt; You may not need to move at all — see UniForm below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything after this is the reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they came from, and why it still matters
&lt;/h2&gt;

&lt;p&gt;This is the part that explains most of the differences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iceberg&lt;/strong&gt; is an Apache Software Foundation project. ASF governance means no single vendor controls the spec, and that shows in the ecosystem: BigQuery, Snowflake, Redshift, Athena, Trino, ClickHouse, DuckDB, Dremio, StarRocks, Doris, Druid, Firebolt and Microsoft OneLake all read it, and the catalog layer has multiple independent implementations including Apache Polaris, Apache Gravitino, AWS Glue, Nessie and Lakekeeper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delta Lake&lt;/strong&gt; came out of Databricks and became a Linux Foundation project in 2019. The project states plainly that it is “an independent open-source project and not controlled by any single company”, and it lists over 190 developers from more than 70 organisations. But its centre of gravity is still Databricks, and its most active recent development — the Unity Catalog Delta APIs — is Databricks-adjacent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means in practice:&lt;/strong&gt; if your worry is “will I be able to read my own data in five years with a tool I have not chosen yet”, Iceberg’s governance is the more conservative bet. If your platform is already Databricks, that worry is mostly theoretical and Delta gives you a better daily experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually the same
&lt;/h2&gt;

&lt;p&gt;More than the marketing suggests. Both give you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACID transactions&lt;/td&gt;
&lt;td&gt;Writers do not corrupt each other; readers see a consistent snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time travel&lt;/td&gt;
&lt;td&gt;Query the table as it looked yesterday; roll back a bad load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema evolution&lt;/td&gt;
&lt;td&gt;Add, rename, drop and reorder columns without rewriting data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row-level deletes and updates&lt;/td&gt;
&lt;td&gt;Delete or update individual rows without rewriting whole files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming and batch&lt;/td&gt;
&lt;td&gt;The same table serves both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parquet underneath&lt;/td&gt;
&lt;td&gt;Your actual data files are the same in both&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If someone tells you one of these is a differentiator, they are selling something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually different
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm31c747mnstlhh8bn0jr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm31c747mnstlhh8bn0jr.webp" alt="What Iceberg and Delta Lake really do differently: partitioning, governance and interoperability." width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things genuinely differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Iceberg hides partitioning; Delta does not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is Iceberg’s most useful distinctive feature and the one most worth understanding.&lt;/p&gt;

&lt;p&gt;In older systems, if a table was partitioned by day, a query had to filter on the partition column explicitly or it would scan everything. Users had to know the physical layout.&lt;/p&gt;

&lt;p&gt;Iceberg records the &lt;em&gt;transform&lt;/em&gt; — “partition by day of this timestamp column” — as table configuration. You write a normal filter on the timestamp, and Iceberg derives the partition filter and skips files for you.&lt;/p&gt;

&lt;p&gt;Better still, it supports &lt;strong&gt;partition evolution&lt;/strong&gt;. Start partitioning by month, discover your data grew, switch to day — without rewriting the existing data. Old files keep their old scheme, new files use the new one, and queries still work because filters are derived, not hard-coded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Delta answers interoperability with UniForm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Delta’s response to “but everyone else uses Iceberg” is the Delta Universal Format. UniForm lets Delta tables be read by Iceberg and Hudi clients.&lt;/p&gt;

&lt;p&gt;That is a genuinely pragmatic move. If your organisation standardised on Delta three years ago, you do not necessarily need a migration project — you may just need to turn on UniForm and let Iceberg-speaking engines read what you already have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Delta ships a kernel that engines embed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Delta maintains a Kernel, including a Rust implementation, that other engines can embed rather than reimplementing the protocol. ClickHouse integrated the Rust Delta Kernel this year. This lowers the cost of a new engine supporting Delta correctly, which matters because protocol reimplementation is where subtle bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that actually decides it
&lt;/h2&gt;

&lt;p&gt;Here is the part most comparisons skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You will change your table format more easily than you will change your catalog.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The catalog is what tracks which tables exist, where their metadata lives, and who is allowed to read them. Iceberg has a REST Catalog specification precisely to decouple this, and there are several implementations. Delta’s recent work centres on the Unity Catalog Delta APIs.&lt;/p&gt;

&lt;p&gt;Your catalog is wired into your permission model, your lineage tooling, your CI, and every pipeline you run. Migrating a table format is a documented procedure — Iceberg even publishes a Delta Lake migration guide. Migrating a catalog is a project.&lt;/p&gt;

&lt;p&gt;So the honest decision procedure is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What catalog will govern this data?&lt;/strong&gt; If the answer is Unity Catalog, you are on Delta. If it is Polaris, Glue, Gravitino, BigLake or Nessie, you are on Iceberg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which engines must read it?&lt;/strong&gt; List them, then check which format each speaks natively rather than through a bridge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then&lt;/strong&gt; look at features.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you skip to step 3 you will make a decision that step 1 quietly overrules six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which spec version to be on
&lt;/h2&gt;

&lt;p&gt;Worth stating because it trips people up.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Iceberg&lt;/strong&gt; , spec versions 1, 2 and 3 are complete and adopted by the community. Version 3 added extended types (nanosecond timestamps, variant, geometry and geography), column default values, row lineage tracking and binary deletion vectors. &lt;strong&gt;Version 4 is under active development and has not been formally adopted&lt;/strong&gt; — it restructures metadata and adds relative paths so tables can be relocated without rewriting metadata. Do not plan around v4 yet.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Delta Lake&lt;/strong&gt; , the current line is &lt;strong&gt;4.4.0 on Apache Spark 4.2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Databricks is your platform&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Delta Lake&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple query engines, several vendors&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Iceberg&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery or Snowflake as primary warehouse&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — both read it natively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need to change partitioning later&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — partition evolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already on Delta, need Iceberg readers&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Stay on Delta&lt;/strong&gt; , enable UniForm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance and vendor neutrality are the priority&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — ASF governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starting fresh, no constraints&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Iceberg&lt;/strong&gt; — broader native support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your catalog is Unity Catalog&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Delta Lake&lt;/strong&gt; — the decision is already made&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are still deciding what sits &lt;em&gt;above&lt;/em&gt; the table format, the warehouse comparison in &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/data/bigquery-vs-snowflake-2026-honest-comparison" rel="noopener noreferrer"&gt;BigQuery vs Snowflake&lt;/a&gt; covers the layer that will consume these tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The format war is quieter than the blog posts suggest. Both formats do the same core job, both are production-grade, and the interoperability layers mean picking “wrong” is recoverable.&lt;/p&gt;

&lt;p&gt;What is not recoverable cheaply is a catalog choice, an engine commitment, or a permission model built around the wrong assumption. Decide those first and the table format usually decides itself.&lt;/p&gt;

&lt;p&gt;And if you take one technical thing away: &lt;strong&gt;hidden partitioning and partition evolution are the real Iceberg advantage.&lt;/strong&gt; Not because they are exciting, but because “we partitioned this wrong two years ago and cannot fix it without a rewrite” is a genuinely expensive sentence, and Iceberg is the one that lets you avoid saying it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.alekseialeinikov.com/en/blog/topics/data/iceberg-vs-delta-lake-2026" rel="noopener noreferrer"&gt;alekseialeinikov.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>apacheiceberg</category>
      <category>deltalake</category>
      <category>tableformat</category>
      <category>lakehouse</category>
    </item>
  </channel>
</rss>
