Cosmos DB Partitionierung richtig gemacht: Der häufigste Fehler und wie man ihn vermeidet
Du hast Cosmos DB aufgesetzt, alles läuft stabil, die Queries antworten schnell. Dann kommt der erste große Load-Test, oder einfach zwei Wochen Produktion mit realem Wachstum, und plötzlich häufen sich HTTP 429-Fehler und die Azure-Rechnung klettert unerwartet hoch. Der Fehler liegt fast immer schon vor dem ersten Write begraben. Er steckt im Partition Key.
Das Tückische daran ist der Zeitpunkt. Bei kleinen Datenmengen und überschaubarem Traffic sieht alles gut aus. Die Probleme zeigen sich erst mit Skalierung, genau dann, wenn du sie am wenigsten gebrauchen kannst. Und anders als bei einem überdimensionierten VM-SKU kannst du den Partition Key eines bestehenden Containers nach der Erstellung nicht mehr ändern. Eine Migration kostet Zeit, Nerven und Ausfallzeit.
Warum der Partition Key alles entscheidet
Cosmos DB speichert Daten intern in logischen Partitionen: Alle Items mit demselben Partition Key-Wert landen in genau einer logischen Partition. Diese logischen Partitionen werden auf physische Partitionen verteilt, die Azure intern verwaltet. Jede physische Partition hat klare Obergrenzen: maximal 10.000 RU/s (Request Units pro Sekunde) Durchsatz und maximal 50 GB Speicher. Eine logische Partition darf dabei nicht mehr als 20 GB belegen.
Der entscheidende Punkt ist die RU-Verteilung: Bei manuell provisioniertem Durchsatz wird das Budget gleichmäßig auf alle physischen Partitionen aufgeteilt. Wenn dein Container 4.000 RU/s hat und intern vier physische Partitionen verwaltet, bekommt jede genau 1.000 RU/s, unabhängig davon, wie stark sie tatsächlich genutzt wird. Eine Hot Partition, auf der der Großteil des Traffics einläuft, stößt damit schnell an ihre Grenze und wird gedrosselt, während die anderen Partitionen den Großteil ihres zugewiesenen Budgets ungenutzt lassen. Du bezahlst Durchsatz, den du nicht nutzen kannst, und wirst gleichzeitig beim Traffic gedrosselt, der tatsächlich anfällt.
Autoscale mildert diesen Effekt nur teilweise, weil das System den verfügbaren Durchsatz zwar dynamisch erhöht, die Last aber weiterhin auf einzelne physische Partitionen konzentriert sein kann. Im Serverless-Modell liegt die Obergrenze pro physischer Partition zusätzlich niedriger, aktuell bei 5.000 RU/s, sodass Hot Partitions dort noch früher sichtbar werden.
Die offizielle Übersicht zu logischen und physischen Partitionen gehört zur Pflichtlektüre vor jeder Cosmos-Implementierung. Weil der Key unveränderlich ist, zählt er zu den wenigen Architekturentscheidungen, die du von Anfang an durchdenken solltest.
Die häufigsten Fehler bei der Wahl des Partition Keys
Die meisten Fehler folgen einem Muster: Es wird das gewählt, was als Feldname intuitiv naheliegt, nicht das, was die Datenverteilung tatsächlich steuert.
Fehler 1: Niedrige Kardinalität. Felder wie region, category, type oder Boolean-Werte haben nur wenige mögliche Werte. Alle Daten einer Kategorie landen auf derselben physischen Partition, unabhängig davon, wie viele Millionen Items das betrifft.
Fehler 2: Zeit- oder Datumsfelder. Ein createdDate auf Tages-Granularität bedeutet: Alle Writes eines Tages landen auf einer einzigen Partition. Bei Systemen mit hohem Schreibvolumen, z. B. Event-Streams oder transaktionalen Apps mit Spitzenlast, ist das besonders kritisch.
Fehler 3: id als Partition Key. Das klingt elegant und sorgt für maximale Kardinalität. Wenn deine Queries aber nicht nach id filtern, scannt jede Query alle physischen Partitionen. Die offiziellen Kriterien für einen geeigneten Partition Key verlangen sowohl hohe Kardinalität als auch Präsenz im häufigsten Query-Filter. id als Key passt nur, wenn du vorwiegend Point Reads machst, also direkte Einzelabrufe per Partition Key und Item-ID. Die genauen Bedingungen dafür erklärt die offizielle Dokumentation: Use item ID as the partition key.
Fehler 4: Veränderliche Felder als Key. Der Key-Wert eines Items kann nach dem Schreiben nicht mehr geändert werden. Wer email als Key wählt, wenn User ihre Adresse anpassen können, steht ohne Migrationsskript vor einem unlösbaren Problem.
Fehler 5: Key ohne Query-Alignment. Du hast dir Mühe gegeben, einen vermeintlich guten Key zu wählen, aber deine wichtigsten Queries filtern nie danach. Das Ergebnis sind ständige Fan-out-Queries, also Abfragen, die mangels Partition-Key-Filter alle physischen Partitionen parallel anfragen.
Die folgende Tabelle zeigt, wie verschiedene Partition Key-Kandidaten für einen typischen Order-Container abschneiden:
| Partition Key | Kardinalität | Write-Eignung | Query-Eignung |
|---|---|---|---|
category | Sehr niedrig | Schlecht | Nur Kategorieabfragen |
createdDate | Niedrig | Schlecht | Zeitbereichsabfragen |
id | Sehr hoch | Gut | Nur Point Reads |
customerId | Hoch | Gut | Kundenzentrierte Queries |
tenantId | Mittel-hoch | Gut | Mandantenabfragen |
Was Hot Partitions wirklich kosten
Rate Limiting ist das sichtbare Symptom. Wenn eine physische Partition ihre 10.000 RU/s-Grenze erreicht, antwortet Cosmos DB mit HTTP 429. Das .NET SDK stellt solche Requests automatisch erneut, aber mit Latenzkosten, die sich schnell zu spürbaren Verzögerungen summieren.
Was weniger auffällt, ist der stille Kostenanstieg. Das Prinzip der gleichmäßigen RU-Aufteilung bedeutet: Eine Hot Partition kostet dich doppelt. Einmal durch Rate Limiting, weil ihr zugewiesener Anteil nicht ausreicht. Und einmal durch verschwendeten Durchsatz auf den anderen Partitionen, der ungenutzt bleibt. Eine Hot Partition ist deshalb kein reines Performance-Problem, sondern immer auch ein Kostenproblem.
Query Fan-out macht das noch teurer. Eine Query ohne Partition Key im Filter muss alle physischen Partitionen parallel anfragen. Je mehr physische Partitionen dein Container hat, desto höher der RU-Verbrauch und desto länger die Latenz. Hot Partitions und Fan-out sind nicht dasselbe, sie tauchen in der Praxis aber oft gemeinsam auf: ein schlechter Partition Key verteilt weder Writes sauber noch passt er zu den wichtigsten Query-Mustern.
Die Zahlen aus dem realen Datenmodellierungsbeispiel auf Microsoft Learn zeigen das an einem verwandten Problem: einer Datenmodellierung, die Fan-out-Queries und teure Abfragen durch fehlende Denormalisierung erzeugt. Sie belegen also nicht die Wirkung einer Hot-Partition-Korrektur, aber sehr gut, wie stark Cosmos-Kosten durch Partitionierungs- und Modellierungsentscheidungen steigen können:
| Query | Ohne Optimierung | Nach Optimierung | Faktor |
|---|---|---|---|
| User-Posts-Abfrage | 619 RU / 130 ms | 6 RU / 4 ms | ~100x |
| Activity-Feed | 2063 RU / 306 ms | 17 RU / 9 ms | ~120x |
Das sind keine theoretischen Werte, sondern reale Messungen aus einem Blogging-Plattform-Beispiel mit 100.000 Nutzern. Hot Partitions sind in Azure Monitor gut sichtbar, wenn du weißt, wo du schaust. Ein Alert auf ungleichmäßige RU-Verteilung gehört zur Standardkonfiguration jedes produktiven Cosmos-Containers. Wie du dabei auch Azure-Kosten breiter optimierst, beschreibt unser Artikel: Azure Kostenoptimierung: Einsparen aber mit Köpfchen.
E-Commerce Orders: Wenn der Partition Key beim Sale-Event versagt
Stell dir einen Online-Shop mit 500.000 Orders pro Tag vor. createdDate auf Tages-Granularität klingt zunächst praktisch: Alle Bestellungen eines Tages landen zusammen, Zeitbereichsabfragen sind günstig. An normalen Tagen funktioniert das: Die eine physische Partition für den Tag hält den Durchsatz ohne Probleme durch. Dann kommt Black Friday.
Mit dem Zehnfachen des üblichen Schreibvolumens treffen plötzlich alle Orders des Tages auf dieselbe physische Partition. Ihr RU/s-Budget ist erschöpft, Cosmos DB drosselt, HTTP 429-Fehler häufen sich. Bei sehr großen Shops kommt zusätzlich die 20-GB-Grenze ins Spiel: Mehr als 20 GB Bestelldaten an einem einzigen Tag, und weitere Writes werden vollständig blockiert.
customerId als Partition Key löst das grundlegend: hohe Kardinalität, gleichmäßige Write-Verteilung unabhängig von Spitzenlast, und die Bestellhistorie eines Kunden ist eine effiziente Single-Partition-Query. So legst du den Container mit dem .NET SDK v3 an:
// Container mit customerId als Partition Key erstellen (.NET SDK v3)
ContainerProperties props = new ContainerProperties("orders", "/customerId");
// Container mit Initial-Throughput bereitstellen
await database.CreateContainerIfNotExistsAsync(props, throughput: 10_000);
Das nächste Problem kommt vom Fulfillment-Team: Alle offenen Bestellungen sortiert nach Datum enthält kein customerId im Filter und wird damit zur Fan-out-Query. Naheliegend wäre jetzt, orderStatus als Partition Key zu wählen, damit Status-Queries gezielt auf eine Partition gehen. Aber orderStatus hat nur wenige Werte (pending, processing, shipped, cancelled), also niedrige Kardinalität aus Fehler 1. Und der Status ändert sich im Laufe einer Bestellung: Ein Item, dessen Key-Wert sich nach dem Schreiben ändern müsste, lässt sich in Cosmos DB nicht aktualisieren (Fehler 4). orderStatus scheidet damit als primärer Partition Key aus.
Die Lösung ist Denormalisierung über Change Feed. Change Feed ist ein eingebauter Änderungs-Stream von Cosmos DB, der neue oder geänderte Dokumente fortlaufend bereitstellt. Du behältst customerId als primären Key für transaktionale Writes und führst einen separaten Container orders-by-status ein. Genau dort ist orderStatus ein valider Partition Key, weil in diesen Container nicht die Transaktionslogik geschrieben wird, sondern eine für Fulfillment optimierte Projektion.
Container sourceContainer = database.GetContainer("shop", "orders");
Container fulfillmentContainer = database.GetContainer("shop", "orders-by-status");
Container leaseContainer = database.GetContainer("shop", "change-feed-leases");
string instanceName = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")
?? Guid.NewGuid().ToString("N");
// Change Feed Processor synchronisiert Orders in den Fulfillment-Container.
ChangeFeedProcessor processor = sourceContainer
.GetChangeFeedProcessorBuilder<Order>(
processorName: "syncToOrdersByStatus",
onChangesDelegate: async (changes, cancellationToken) =>
{
foreach (var order in changes)
{
try
{
OrderByStatus projection = OrderByStatus.From(order);
await fulfillmentContainer.UpsertItemAsync(
projection,
new PartitionKey(projection.Status),
cancellationToken: cancellationToken);
}
catch (CosmosException)
{
// In der Praxis: Fehler protokollieren und fuer Retry oder Dead-Letter markieren.
throw;
}
}
})
.WithInstanceName(instanceName)
.WithLeaseContainer(leaseContainer)
.Build();
await processor.StartAsync();
Das Denormalisierungsmuster mit Change Feed ist ein zentrales Designprinzip für Cosmos DB, wenn verschiedene Query-Patterns verschiedene Partition Keys erfordern. In der Praxis kommen dafür aber ein zweiter Zielcontainer, ein Lease-Container, ein laufender Prozessor sowie Monitoring und ein bewusster Umgang mit der kurzen Replikationsverzögerung hinzu.
| Partition Key | Kardinalität | Write-Peak-Eignung | Query-Eignung |
|---|---|---|---|
orderStatus | Sehr niedrig | Sehr schlecht | Nur Statusfilter |
createdDate (Tag) | Niedrig | Schlecht | Zeitbereichsabfragen |
customerId | Hoch | Gut | Kundenzentrierte Queries |
Multi-Tenant SaaS: Die tenantId-Falle
In einem Multi-Tenant SaaS-System ist tenantId als Partition Key in den meisten Fällen eine gute Wahl: hohe Kardinalität bei vielen Mandanten, isolierte Schreiblast pro Tenant, und Queries nach Tenant sind Single-Partition-Queries. Das spezifische Problem tritt auf, wenn du sehr heterogene Mandantengrößen hast: eine Handvoll Enterprise-Kunden neben vielen kleinen.
Diese Whale Tenants können die 20-GB-Grenze einer logischen Partition überschreiten. Wenn tenantId der Key ist und ein Tenant seine 20 GB erreicht, verweigert Cosmos DB weitere Writes für diesen Wert. Das ist kein Throttling mehr, sondern ein harter Ausfall für genau diesen Kunden. Genau deshalb reicht es in Multi-Tenant-Systemen nicht, nur auf viele Mandanten insgesamt zu schauen. Du musst auch prüfen, wie groß einzelne Mandanten realistischerweise werden können.
Ansatz 1: Synthetischer Key tenantId_entityId erhöht die Kardinalität erheblich. Jede Entity bekommt eine eigene logische Partition. Der Nachteil: Abfragen über alle Entities eines Tenants werden zu Cross-Partition-Queries, was den RU-Verbrauch proportional zur Partitionsanzahl erhöht. Für Systeme ohne Whale Tenants ist das tolerierbar.
Ansatz 2: Hierarchischer Partition Key (HPK) [tenantId, entityId] löst das eleganter. Queries mit tenantId im Filter werden auf die Partitionen dieses Tenants beschränkt, statt alle physischen Partitionen zu scannen. Das offizielle Szenario für Hierarchical Partition Keys zeigt am Beispiel TenantId > UserId > SessionId, wie das Routing-Verhalten funktioniert. Die Bicep-Konfiguration dafür:
partitionKey: {
// Hierarchischer Key: Tenant auf erster, Entity auf zweiter Ebene
paths: ['/tenantId', '/entityId']
kind: 'MultiHash'
version: 2
}
Wichtig zu wissen: HPK funktioniert aktuell nur mit der NoSQL-API, nicht mit der MongoDB- oder Cassandra-API. Er kann außerdem nur beim Erstellen eines Containers gesetzt werden, nicht nachträglich.
| Ansatz | Kardinalität | Whale-Tenant-Schutz | Query-Effizienz pro Tenant |
|---|---|---|---|
tenantId (einfacher Key) | Mittel | Kein Schutz | Optimal |
Synthetisch tenantId_entityId | Sehr hoch | Ja | Cross-Partition |
HPK [tenantId, entityId] | Sehr hoch | Ja | Targeted Cross-Partition |
Weitere Strategien für Multi-Tenant-Szenarien beschreibt das Azure Architecture Center unter Data Partitioning Strategies.
Partition Keys vor dem Go-Live validieren
Die härteste Frage vor dem Go-Live lautet nicht Was sind meine Queries heute?, sondern Wie sieht meine Datenverteilung in 12 Monaten aus? Wer das nicht beantwortet, baut heute eine Architektur, die morgen migriert werden muss.
Der erste Schritt liegt darin, alle Access Patterns zu modellieren. Schreib für jede Query auf: Enthält sie den Partition Key im Filter? Wenn ein Großteil deiner wichtigsten Queries den Key nicht im Filter hat, ist das ein klares Signal, den Key nochmals zu überdenken. Wie du Cross-Partition-Queries technisch vermeidest, beschreibt die Dokumentation: Cross-Partition-Queries in Cosmos DB vermeiden.
Für die technische Validierung bieten sich zwei Ansätze an. Azure Monitor mit Partition Key Statistics visualisiert RU-Verbrauch und Speicher pro logischer Partition und macht Hot Spots direkt sichtbar. Die Schritt-für-Schritt-Anleitung zum Alert-Setup erklärt, wie du einen Alert auf Normalized RU Consumption > 80% für eine einzelne Partition Key Range konfigurierst. Der Cosmos DB Emulator eignet sich für erste lokale Tests, ist aber für Partition-Level-Metriken nicht geeignet.
Beim Load-Test gilt: Fülle repräsentative Stichproben echter Produktionsdaten ein, keine synthetisch gleichmäßig verteilten Test-Daten. Nur dann zeigen sich reale Verteilungsasymmetrien.
Die Entscheidungscheckliste
Bevor du den Partition Key festlegst, beantworte diese Fragen:
Kardinalität: Hat der Partition Key genug eindeutige Werte, um Daten auf viele logische Partitionen zu verteilen? Als Faustregel solltest du eher in Hunderte oder Tausende unterschiedlicher Werte denken, nicht in eine Handvoll Kategorien.
Write-Verteilung: Verteilen sich Writes gleichmäßig auf viele Key-Werte, oder gibt es zeitliche Bursts, bei denen sich das Schreibvolumen auf einen einzigen Wert konzentriert?
Query-Alignment: Enthalten die meisten deiner wichtigsten Queries den Key im Filter? Welche Queries akzeptierst du bewusst als Fan-out?
Unveränderlichkeit: Kann der Key-Wert nach dem Schreiben eines Items jemals geändert werden? Falls ja, ist es als Key ungeeignet.
20-GB-Grenze: Kann ein einzelner Key-Wert jemals 20 GB überschreiten? Falls das möglich ist, plane von Anfang an mit HPK oder synthetischem Key.
Transaktionen: Brauchst du ACID-Transaktionen (Atomicity, Consistency, Isolation, Durability) über mehrere Items hinweg? Diese sind nur innerhalb einer logischen Partition möglich, also nur für Items mit demselben Key-Wert.
Für transaktionale Anwendungen wie E-Commerce empfiehlt sich meist customerId, kombiniert mit Denormalisierung via Change Feed für alternative Query-Patterns. orderId ist nur dann sinnvoll, wenn das System fast ausschließlich aus Point Reads oder Einzelverfolgung pro Bestellung besteht. Für Multi-Tenant SaaS reicht tenantId als Key in den meisten Fällen aus. HPK [tenantId, entityId] lohnt sich gezielt dann, wenn Whale Tenants erwartet werden, die die 20-GB-Grenze einer logischen Partition erreichen könnten.
Als Ausblick lohnen sich die Global Secondary Indexes (Preview). Sie könnten langfristig die manuelle Denormalisierung für alternative Query-Patterns ablösen. Die Funktion ist heute noch in der Preview, zeigt aber, wohin die Entwicklung geht. Wenn du das Thema methodisch vertiefen willst, bietet das Microsoft Learn Modul zur Datenpartitionierungsstrategie einen guten strukturierten Einstieg.
Der Partition Key ist eine der wenigen Entscheidungen in Cosmos DB, bei denen Nachbessern Ausfallzeiten kostet. Die Investition in eine sorgfältige Analyse vor dem ersten Byte lohnt sich immer.
Wie handhabst du die Partitionierungsstrategie in deinen Cosmos-Projekten? Schau gerne bei uns auf LinkedIn vorbei und diskutiere mit.
Autoren
Felix Burkhard
Felix ist Solution Architect mit Fokus auf skalierbare Azure-IoT-Lösungen, agile Entwicklungsprozesse und DevOps. Er verbindet tiefes technisches Verständnis mit pragmatischer Umsetzung und schafft so nachhaltigen Geschäftsnutzen.