<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>TheCodeBuzz</title>
	<atom:link href="https://thecodebuzz.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://thecodebuzz.com</link>
	<description>Best Practices for Software Development</description>
	<lastBuildDate>Wed, 11 Nov 2026 14:59:52 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://thecodebuzz.com/wp-content/uploads/2022/11/cropped-android-chrome-512x512-1-1-51x51.jpg</url>
	<title>TheCodeBuzz</title>
	<link>https://thecodebuzz.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Cosmos DB Best Practices: Partitioning, RU and Consistency</title>
		<link>https://thecodebuzz.com/cosmos-db-best-practices-partitioning/</link>
					<comments>https://thecodebuzz.com/cosmos-db-best-practices-partitioning/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Wed, 11 Nov 2026 14:59:46 +0000</pubDate>
				<category><![CDATA[Database Best Practices]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Cosmos DB]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[Partition Key]]></category>
		<category><![CDATA[Request Units]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30995</guid>

					<description><![CDATA[<p>Cosmos DB best practices for production: choose a partition key, fix hot partitions, cut RU cost, handle 429 errors and pick a consistency level.</p>
<p>The post <a href="https://thecodebuzz.com/cosmos-db-best-practices-partitioning/">Cosmos DB Best Practices: Partitioning, RU and Consistency</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Today in this article, we will see a practical Cosmos DB best practices guide for Azure Cosmos DB for NoSQL, covering partition keys, RU optimization and <a href="https://thecodebuzz.com/cassandra-best-practices-guidelines/">consistency levels</a>. It is written for engineers, architects and DBAs who run Cosmos DB in production or are about to.</p>
<p>Three decisions drive most production problems. The first is the <strong>partition key</strong>, which cannot be changed in place once a container exists. The second is the <strong>RU cost of each operation</strong>, since every read and write is billed in Request Units. The third is the <strong>consistency level</strong>, which trades read freshness against latency and availability.</p>
<p>The guide starts with an at-a-glance table. It then covers partition keys and hierarchical partition keys, hot partitions and <a href="https://thecodebuzz.com/what-is-data-modelling-fundamentals-sql-nosql-guidelines/">data modeling</a>, and RU and 429 handling with .NET SDK v3 code. After that come consistency levels, followed by anti-patterns, a production readiness checklist and a migration note. You will not find generic savings figures here. Instead, the guide shows you how to measure your own RU numbers, because the cost depends on your data and queries.</p>
<p>Before we start, you need an existing Cosmos DB for NoSQL account and the .NET SDK v3 (<code>Microsoft.Azure.Cosmos</code>). The current SDK version was not confirmed while writing, so check NuGet for the latest release.</p>
<blockquote><p><strong>Version note:</strong> This guide targets Azure Cosmos DB for NoSQL (a managed service with no user-facing version number, with features announced up to Build 2026 in June 2026) and the .NET SDK v3 (Microsoft.Azure.Cosmos), whose current version you should confirm on NuGet.</p>
<p>&nbsp;</p>
<p><strong>In this guide</strong></p></blockquote>
<ul>
<li><a href="#cosmos-db-best-practices-at-a-glance">Cosmos DB Best Practices at a Glance</a></li>
<li><a href="#cosmos-db-partition-key-best-practices">Cosmos DB Partition Key Best Practices</a></li>
<li><a href="#fix-a-cosmos-db-hot-partition-with-a-better-data-model">Fix a Cosmos DB Hot Partition with a Better Data Model</a></li>
<li><a href="#cosmos-db-ru-optimization-and-429-throttling">Cosmos DB RU Optimization and 429 Throttling</a></li>
<li><a href="#cosmos-db-consistency-levels-choose-and-override">Cosmos DB Consistency Levels: Choose and Override</a></li>
<li><a href="#common-anti-patterns-operations-and-production-readiness-che">Common Anti-Patterns, Operations and Production Readiness Checklist</a></li>
<li><a href="#key-takeaways">Key takeaways</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="cosmos-db-best-practices-at-a-glance">Cosmos DB Best Practices at a Glance</h2>
<p>Most Cosmos DB problems come from a weak partition key, expensive queries and the wrong consistency level. The table below sums up the rules for Cosmos DB cost optimization and performance.</p>
<p>Here is a quick reference of what to do and what to avoid in each area:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Area</th>
<th>Do this</th>
<th>Avoid this</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Partition key</strong></td>
<td>Pick a high-cardinality key that spreads storage and RU use evenly. Keep each logical partition under 20 GB, unless you use a hierarchical key that ends in <code>/id</code>, which lets the earlier levels grow past that limit.</td>
<td>Low-cardinality or purely time-based keys.</td>
</tr>
<tr>
<td><strong>Hierarchical keys</strong></td>
<td>Use up to three levels, each with high cardinality, ordered by filter use.</td>
<td>A first level that only a few values ever touch.</td>
</tr>
<tr>
<td><strong>Data model</strong></td>
<td>Colocate related entity types in one container or a few.</td>
<td>One container per entity type.</td>
</tr>
<tr>
<td><strong>Reads</strong></td>
<td>Use point reads (id plus partition key). Project only the fields you need.</td>
<td><code>SELECT *</code> and cross-partition queries.</td>
</tr>
<tr>
<td><strong>Indexing policy</strong></td>
<td>Exclude paths you never filter or sort on.</td>
<td>Indexing everything on write-heavy data.</td>
</tr>
<tr>
<td><strong>Throughput mode</strong></td>
<td>Match RU/s to the real workload shape.</td>
<td>Paying for peak capacity all day.</td>
</tr>
<tr>
<td><strong>429 handling</strong></td>
<td>Honor the retry-after value and tune SDK retry settings.</td>
<td>Adding RU/s to fix a hot partition.</td>
</tr>
<tr>
<td><strong>Consistency</strong></td>
<td>Choose the weakest account default your correctness rules allow. Relax it per request for reads that can tolerate it.</td>
<td>Strong consistency everywhere.</td>
</tr>
<tr>
<td><strong>Monitoring</strong></td>
<td>Log the RU charge from every response.</td>
<td>Guessing where RUs go.</td>
</tr>
<tr>
<td><strong>Migration</strong></td>
<td>Decide the key early. Copy data to a new container if it must change.</td>
<td>Expecting an in-place key change.</td>
</tr>
</tbody>
</table>
</figure>
<p>Each row is explained with its reason in the sections below.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>In plain English:</strong> A Request Unit (RU) is one normalized currency for the CPU, IO and memory an operation needs. Think of it like a restaurant bill: reading one small item is a cheap dish, a large query is a full banquet, and your throughput is the budget per second.</p></blockquote>
<h2 id="cosmos-db-partition-key-best-practices">Cosmos DB Partition Key Best Practices</h2>
<p>Choose a partition key with <strong>high cardinality</strong> (many distinct values) that spreads both storage and RU use evenly and appears in your most common query filters. The key is hard to change later, so it deserves the most design time of any Cosmos DB decision.</p>
<h3>What Is the Difference Between a Logical and a Physical Partition?</h3>
<p>A <strong>logical partition</strong> holds all items that share the same partition key value, and it is limited to 20 GB. A <strong>physical partition</strong> is the storage and compute unit that hosts one or more logical partitions. You control the first, and Cosmos DB manages the second.</p>
<blockquote><p><strong>In plain English:</strong> Think of a library. Each logical partition is a shelf of labelled books, and each physical partition is a room that holds several shelves. You choose the labels, and the library decides which room each shelf goes in. Unlike a real shelf, a logical partition can never grow past its 20 GB limit.</p></blockquote>
<h3>How Do You Know a Partition Key Is Good?</h3>
<p>A good key passes four checks. Test your candidate against each one before you create the container.</p>
<ul>
<li><strong>High cardinality:</strong> many distinct values mean many small logical partitions that can be spread across physical partitions.</li>
<li><strong>Even request distribution:</strong> no value should receive far more reads or writes than the others, or one partition throttles while the rest sit idle.</li>
<li><strong>Even storage distribution:</strong> no value should approach the 20 GB logical partition limit.</li>
<li><strong>Used in filters:</strong> a query that includes the key stays in one partition. Without it, the query becomes a cross-partition query that fans out to every partition and costs more RUs.</li>
</ul>
<p>The animated diagram below walks through these checks as a decision tree, from your top queries to a single-property key, a hierarchical key, or a data model rework.</p>
<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cosmos-db-best-practices-partitioning-diagram-2.gif" alt="Decision tree: test filters, cardinality, and spread to pick a single-property key, a hierarchical key, or rework the data model." width="1200" height="675" /><figcaption>Work through steps 1 to 3 in order; each answer leads to a single key, a hierarchical key, or a data model rework.</figcaption></figure>
<h3>Worked Example: Which Key Fits an IoT Workload?</h3>
<p>Microsoft&#8217;s IoT partitioning example compares three options for a system ingesting data from more than 50K devices. Only the third passes every check.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Candidate key</th>
<th>Result</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>DeviceId only</strong></td>
<td>Rejected</td>
<td>Data from a device keeps accumulating, so with 50K+ devices there is a risk of hitting the 20 GB limit.</td>
</tr>
<tr>
<td><strong>Time-based only</strong></td>
<td>Rejected</td>
<td>All current writes land in one partition.</td>
</tr>
<tr>
<td><strong>DeviceId plus time-based second level</strong></td>
<td>Chosen</td>
<td>Balances even distribution with efficient queries.</td>
</tr>
</tbody>
</table>
</figure>
<h3>When Should You Use Hierarchical Partition Keys?</h3>
<p>Use <strong>Cosmos DB hierarchical partition keys</strong> when one property cannot satisfy the checks alone. They are generally available and allow up to three levels (e.g. TenantId, UserId, SessionId). They replace many synthetic-key designs, especially in multi-tenant workloads.</p>
<ul>
<li><strong>Keep every level high cardinality:</strong> this applies from the first level onward.</li>
<li><strong>Watch the first level&#8217;s real traffic:</strong> a first level that looks high cardinality can still be hot if the workload touches only a few values.</li>
<li><strong>Order levels by filter use:</strong> put the level that appears most often in your filters first, in the order you filter on them.</li>
</ul>
<p>Items that share the same first-level value sit on the same physical partition, so queries on that value avoid a full fan-out. If the key ends with a unique value such as <code>/id</code>, the earlier levels can grow past 20 GB. No account-level setting is needed.</p>
<p>The code below creates a container with a three-level key (TenantId, UserId, id) and reads one item by building the full key. Use a recent version of the <code>Microsoft.Azure.Cosmos</code> package from NuGet.</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">using Microsoft.Azure.Cosmos;

CosmosClient client = new CosmosClient(endpoint, key);
Database database = client.GetDatabase("appdb");

ContainerProperties properties = new ContainerProperties(
    id: "sessions",
    partitionKeyPaths: new List&lt;string&gt; { "/TenantId", "/UserId", "/id" });

ContainerResponse created =
    await database.CreateContainerIfNotExistsAsync(properties, throughput: 400);
Container container = created.Container;

PartitionKey partitionKey = new PartitionKeyBuilder()
    .Add("tenant-42")
    .Add("user-1001")
    .Add("session-9")
    .Build();

ItemResponse&lt;Session&gt; response =
    await container.ReadItemAsync&lt;Session&gt;("session-9", partitionKey);

public record Session(string id, string TenantId, string UserId);</code></pre>
<p>The <code>PartitionKeyBuilder</code> values follow the path order, and the last value matches the item&#8217;s <code>id</code>.</p>
<blockquote><p><strong>Warning:</strong> You cannot change the partition key of an existing container in place. To change it, create a new container with the right key and copy the data across, so decide early and test your key against real access patterns first.</p></blockquote>
<h2 id="fix-a-cosmos-db-hot-partition-with-a-better-data-model">Fix a Cosmos DB Hot Partition with a Better Data Model</h2>
<p>A <strong>Cosmos DB hot partition</strong> is one partition that takes far more RU or storage than the others. It starts to throttle while the rest of the container sits idle.</p>
<blockquote><p><strong>In plain English:</strong> Picture a supermarket with ten checkout lanes where every customer joins lane one. Lane one has a long queue while nine cashiers wait. Adding more cashiers doesn&#8217;t help, because customers still pick the same lane.</p></blockquote>
<h3>How do you detect a hot partition?</h3>
<p>Compare partition-level metrics with the container-wide numbers in Azure Monitor (Metrics). Look at normalized RU consumption split by partition.</p>
<ul>
<li><strong>Skew pattern:</strong> a few partitions sit near 100% while the container average stays low. This points to a skewed key, not a lack of capacity.</li>
<li><strong>Matching symptom:</strong> 429 responses appear even though total consumption looks well below what you provisioned.</li>
</ul>
<h3>Why doesn&#8217;t raising RU/s fix it?</h3>
<p>Provisioned throughput is divided across physical partitions, so the hot partition still gets only its own share. Extra RU/s mostly pays for capacity the idle partitions never use. That is why this is a data model problem first and a Cosmos DB cost optimization problem second.</p>
<p>A Cosmos DB Conf 2026 production lesson shows this. One logical partition took over 80% of traffic. After the data model was fixed, with no extra scaling, RU utilization fell to 20-35% and throttling stopped.</p>
<h3>What should you change in the data model?</h3>
<p>Change the key or the shape of the data, in this order of preference:</p>
<ol>
<li><strong>Pick a higher-cardinality key:</strong> choose a property with many distinct values that your workload actually touches evenly.</li>
<li><strong>Use hierarchical partition keys:</strong> add levels such as TenantId, UserId, id so one large tenant spreads across many partitions.</li>
<li><strong>Add a spreading level:</strong> a time bucket (e.g. day or month) stops current writes from landing on one value.</li>
<li><strong>Split read and write shapes:</strong> when one key can&#8217;t serve both, use the design below.</li>
</ol>
<h3>Should you colocate entity types in one container?</h3>
<p>Yes, colocate related entity types in one container, or a small number of containers. Queries are scoped to one container, and multi-item atomic transactions (stored procedures and triggers) are scoped to one partition key in one container. Colocation also cuts network round trips.</p>
<p>If you can&#8217;t colocate, for example when migrating an existing app unchanged, consider database-level shared throughput.</p>
<h3>What is the write-optimized container with change feed design?</h3>
<p>Write to one container whose key spreads writes evenly. Then feed other containers through the change feed, each keyed for one read pattern. The animated diagram below shows this flow.</p>
<figure class="wp-block-image size-full"><img decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cosmos-db-best-practices-partitioning-diagram-3.gif" alt="Writes enter an Orders container, the change feed processor copies items to containers keyed by CustomerId and ProductId, and reads hit them." width="1200" height="675" /><figcaption>Writes land in one evenly partitioned container; the change feed builds read models so each query stays single-partition.</figcaption></figure>
<p>Example: a retail orders container is partitioned for even writes. The change feed processor copies each order into one container keyed by CustomerId and another keyed by ProductId. Both read paths then run as single-partition queries.</p>
<p>Microsoft announced the All Versions and Deletes change feed mode as generally available at Build 2026. Confirm its status in the announcement before you rely on it.</p>
<p><strong>Trade-off:</strong> read-model containers add storage cost and eventual lag, since copies arrive after the write. Use them only when one partition key cannot serve both access patterns.</p>
<h2 id="cosmos-db-ru-optimization-and-429-throttling">Cosmos DB RU Optimization and 429 Throttling</h2>
<p>To <strong>reduce Cosmos DB request units</strong>, use point reads instead of queries, keep queries inside one partition, project only the fields you need and trim the indexing policy. Each of these lowers the RU charge, and RU charge drives both cost and throttling.</p>
<h3>How Do You Measure Cosmos DB RU Cost?</h3>
<p>Read the <code>RequestCharge</code> from every response. The same query on the same data always costs the same RUs, so the number is repeatable and safe to compare before and after a change.</p>
<p>Query execution metrics show where the RUs went. Paged results are charged per page, and the pages add up to the cost of the whole query. Measure your own workload instead of copying generic savings figures.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>In plain English:</strong> An RU is like a price tag on each operation. A cheap lookup costs a few coins, a big scan costs many, and your provisioned RU/s is the coins you get each second.</p></blockquote>
<h3>Point Read vs Query: Which Costs Less?</h3>
<p>A point read is the cheapest way to fetch one item. The table compares the three access shapes.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Operation</th>
<th>Needs</th>
<th>Cost behavior</th>
<th>Use when</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Point read</strong></td>
<td>id and partition key</td>
<td>Lowest for one item</td>
<td>You know both values</td>
</tr>
<tr>
<td><strong>Single-partition query</strong></td>
<td>Partition key in the filter</td>
<td>Grows with items scanned and returned</td>
<td>You need a list inside one key value</td>
</tr>
<tr>
<td><strong>Cross-partition query</strong></td>
<td>Partition key not in the filter</td>
<td>Grows with the number of partitions touched and how selective the filter is; a query that fans out to many partitions costs more than the same query scoped to one</td>
<td>Rare, admin or reporting needs</td>
</tr>
</tbody>
</table>
</figure>
<p>Query cost depends on the number and kind of predicates, the number of UDFs and the size of the source data. A query returning a thousand 1-KB items costs about 1,000 RUs, so at 1,000 RU/s you get roughly one such query per second before throttling. Use <code>SELECT c.id, c.name, c.price</code> instead of <code>SELECT *</code>.</p>
<p><strong>Use case:</strong> A retail catalog service serves product pages by id and category. With category as the partition key, the product page is a point read, and the category listing is a single-partition query.</p>
<h3>How Do You Trim the Indexing Policy?</h3>
<p><a href="https://thecodebuzz.com/mongodb-indexes-performance-guidelines-and-best-practices/">Exclude paths you never filter</a> or sort on. Every indexed path is maintained on each write, so fewer paths mean fewer write RUs.</p>
<p class="code-label"><strong>JSON</strong></p>
<pre><code class="language-json">{
  "indexingMode": "consistent",
  "includedPaths": [ { "path": "/*" } ],
  "excludedPaths": [ { "path": "/descriptionHtml/*" } ]
}</code></pre>
<p>This simplified policy indexes everything except a large, unused description field.</p>
<h3>Which Throughput Mode Should You Use?</h3>
<p>Throughput is set at container level or shared at database level, and you can change it at any time. You pay for what you configure, so match RU/s to the real workload shape. Verify current limits on the pricing page.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Mode</th>
<th>How it works</th>
<th>Use when</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Manual provisioned</strong></td>
<td>You set a fixed RU/s</td>
<td>Traffic is steady and predictable</td>
</tr>
<tr>
<td><strong>Autoscale</strong></td>
<td>RU/s adjusts within a range you set</td>
<td>Traffic varies or has peaks</td>
</tr>
<tr>
<td><strong>Serverless</strong></td>
<td>You pay per request, with no provisioned RU/s</td>
<td>Traffic is small or occasional, e.g. dev and test</td>
</tr>
</tbody>
</table>
</figure>
<h3>What Happens When Cosmos DB Returns 429?</h3>
<p>When a request exceeds provisioned throughput, the service ends it with <strong>HTTP 429 (RequestRateTooLarge)</strong> and an <code>x-ms-retry-after-ms</code> header with the wait time in milliseconds. The SDK retry settings decide how many retries happen before the exception reaches your app.</p>
<p>The animated diagram below shows how a throttled request is retried, and what happens when retries run out.</p>
<figure class="wp-block-image size-full"><img decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cosmos-db-best-practices-partitioning-diagram-4.gif" alt="Sequence across Application, SDK and service lanes: request gets 429, SDK waits, retry returns 200; alternate path ends in CosmosException." width="1200" height="675" /><figcaption>The SDK honors x-ms-retry-after-ms and retries; once retry limits are hit, the 429 CosmosException reaches your code.</figcaption></figure>
<p>Workloads that aren&#8217;t latency-sensitive can run with less throughput and rely on this rate limiting. If 429s come from one hot partition, fix the key. More RU/s is split across partitions and won&#8217;t help.</p>
<h3>How Do You Log RU Charge and Handle 429 in .NET?</h3>
<p>Check NuGet for the current <code>Microsoft.Azure.Cosmos</code> version before you start. First, set the retry options on the client:</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">// Run this once at application startup, not per request.
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
    MaxRetryAttemptsOnRateLimitedRequests = 5,
    MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromSeconds(10)
});
Container container = client.GetContainer("retail", "products");</code></pre>
<p>The SDK stops retrying a throttled call once it has made 5 retries or waited 10 seconds in total, whichever comes first, and then throws.</p>
<p><strong>Create one <code>CosmosClient</code> per application lifetime and reuse it.</strong> The client is designed to be shared and is thread-safe, so register it as a singleton (e.g. in your dependency injection container) and inject the <code>Container</code> where needed. Creating a new client for every request wastes connections and adds startup cost to each call. The snippet above shows the client only to demonstrate the options, and the later examples assume the same shared <code>container</code>.</p>
<p>The values 5 and 10 seconds are example settings, not the SDK defaults. The research notes don&#8217;t list the default values, so check the current Cosmos DB .NET SDK documentation for them. Raising the limits makes the SDK wait longer and hide more throttling from your app, and lowering them makes 429 exceptions reach your code sooner. Pick values that fit how long a caller can wait.</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">public record Product(string id, string category, string name, decimal price);

ItemResponse&lt;Product&gt; response = await container.ReadItemAsync&lt;Product&gt;(
    "sku-1001", new PartitionKey("shoes"));
Console.WriteLine($"Point read RU: {response.RequestCharge}");</code></pre>
<p>This point read logs its own RU charge.</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">public record ProductSummary(string id, string name, decimal price);

var query = new QueryDefinition(
    "SELECT c.id, c.name, c.price FROM c WHERE c.category = @category")
    .WithParameter("@category", "shoes");
var options = new QueryRequestOptions { PartitionKey = new PartitionKey("shoes") };

double totalRu = 0;
using FeedIterator&lt;ProductSummary&gt; iterator =
    container.GetItemQueryIterator&lt;ProductSummary&gt;(query, requestOptions: options);
while (iterator.HasMoreResults)
{
    FeedResponse&lt;ProductSummary&gt; page = await iterator.ReadNextAsync();
    totalRu += page.RequestCharge;
}
Console.WriteLine($"Query RU: {totalRu}");</code></pre>
<p>This single-partition query projects three fields and adds up the charge for every page.</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">try
{
    await container.ReadItemAsync&lt;Product&gt;("sku-1001", new PartitionKey("shoes"));
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
    Console.WriteLine($"Throttled. Retry after: {ex.RetryAfter}");
    Console.WriteLine(ex.Diagnostics.ToString());
    // Back off here before trying again.
}</code></pre>
<p>This catch block runs only after the SDK has used up its retries, so log the diagnostics and back off.</p>
<h2 id="cosmos-db-consistency-levels-choose-and-override">Cosmos DB Consistency Levels: Choose and Override</h2>
<p>Cosmos DB offers five consistency levels: Strong, Bounded Staleness, Session, Consistent Prefix and Eventual. <strong>Session is the default</strong>. Pick the weakest level your correctness rules allow for each read, because stronger levels cost more.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>In plain English:</strong> Consistency is how quickly an update you save shows up for everyone else. Think of a news site: one screen shows the new headline, and consistency decides how long another screen may still show the old one. Unlike a news site, Cosmos DB lets you choose that wait.</p></blockquote>
<h3>What Does Each Consistency Level Guarantee?</h3>
<p>The table below describes what a reader sees at each level and where it fits.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Level</th>
<th>What readers see</th>
<th>Typical use case</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Strong</strong></td>
<td>Always the latest committed write</td>
<td>Account balances, inventory that cannot oversell</td>
</tr>
<tr>
<td><strong>Bounded Staleness</strong></td>
<td>Data that lags by a limited amount, never more</td>
<td>Dashboards that tolerate a known delay</td>
</tr>
<tr>
<td><strong>Session</strong></td>
<td>Your own writes, in order, within your session</td>
<td>Most apps, e.g. shopping cart, user profile</td>
</tr>
<tr>
<td><strong>Consistent Prefix</strong></td>
<td>Updates in order, but possibly behind</td>
<td>Activity feeds where order matters</td>
</tr>
<tr>
<td><strong>Eventual</strong></td>
<td>No ordering promise, converges later</td>
<td>Like counts, view counts</td>
</tr>
</tbody>
</table>
</figure>
<h3>What Are the Trade-offs?</h3>
<p>Stronger levels add latency and reduce availability. Strong is also not available when the account uses multi-region writes. <strong>Strong and Bounded Staleness reads cost more RUs than reads at the weaker levels</strong>, so a stricter default has a price on every read that does not override it.</p>
<p>The size of that difference depends on your data and access pattern. Measure <code>RequestCharge</code> for your own reads at each level instead of trusting a generic figure.</p>
<h3>How Do You Override Consistency Per Request?</h3>
<p>You can set the consistency level on a single request with <code>ItemRequestOptions.ConsistencyLevel</code>. The rule is simple: <strong>a per-request override can only be equal to or weaker than the account default, never stronger</strong>.</p>
<p>This means the account default is your ceiling. If the default is Session, you cannot raise a single request to Strong. To have any request read at Strong, the account default itself must be Strong.</p>
<p>So the design works in one direction. Set the account default to the strictest level your most sensitive reads need, then relax it per request for reads that can tolerate staleness. Here is a simplified example for a retail review feed:</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">var options = new ItemRequestOptions
{
    ConsistencyLevel = ConsistencyLevel.Eventual
};

ItemResponse&lt;Review&gt; response = await container.ReadItemAsync&lt;Review&gt;(
    id: "review-1001",
    partitionKey: new PartitionKey("product-42"),
    requestOptions: options);

Console.WriteLine($"RU charge: {response.RequestCharge}");</code></pre>
<p>This reads one review at Eventual consistency, which is weaker than any account default, and logs its RU charge so you can compare levels.</p>
<blockquote><p><strong>Example:</strong> A banking service sets the account default to Strong, because balance checks must always see the latest committed write (this needs an account without multi-region writes). Reads that tolerate delay, such as a statement history page, relax to Session or Eventual on each request to save latency and RUs. A retail product-review feed relaxes to Eventual the same way, since a review appearing a few seconds late harms nobody.</p></blockquote>
<h2 id="common-anti-patterns-operations-and-production-readiness-che">Common Anti-Patterns, Operations and Production Readiness Checklist</h2>
<p>Most production incidents trace back to a few repeated design mistakes. The table below lists each one with the symptom you will see and the fix.</p>
<h3>Which Cosmos DB anti-patterns cause the most trouble?</h3>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Anti-pattern</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Low-cardinality or time-only key</strong></td>
<td>One partition takes all writes or nears 20 GB</td>
<td>Use a high-cardinality or hierarchical key</td>
</tr>
<tr>
<td><strong>Key not used in filters</strong></td>
<td>Cross-partition fan-out, high RU per query</td>
<td>Pick a key your common filters contain</td>
</tr>
<tr>
<td><strong>SELECT * and queries instead of point reads</strong></td>
<td>High RequestCharge for simple lookups</td>
<td>Use <code>ReadItemAsync</code>, project only needed fields</td>
</tr>
<tr>
<td><strong>One container per entity type</strong></td>
<td>Many round trips, no cross-entity transactions</td>
<td>Colocate related entities</td>
</tr>
<tr>
<td><strong>Default indexing on write-heavy data</strong></td>
<td>High write RU cost</td>
<td>Exclude paths you never filter or sort on</td>
</tr>
<tr>
<td><strong>Raising RU/s to fix a hot partition</strong></td>
<td>429s continue, cost rises</td>
<td>Fix the key or data model</td>
</tr>
<tr>
<td><strong>Never logging RequestCharge</strong></td>
<td>Cost surprises with no cause</td>
<td>Log charge and diagnostics per operation</td>
</tr>
<tr>
<td><strong>Strong consistency everywhere</strong></td>
<td>Higher latency, less availability</td>
<td>Set the default to the weakest level that is acceptable and relax per request</td>
</tr>
</tbody>
</table>
</figure>
<blockquote><p><strong>In plain English:</strong> A cross-partition query is like asking every room in a library to search its shelves for you. A good key sends you to the one room that holds your book.</p></blockquote>
<h3>What should you monitor in production?</h3>
<p>Watch three signals: normalized RU consumption, 429 counts and provisioned RU/s.</p>
<ul>
<li><strong>Normalized RU consumption:</strong> check it per partition, not only as a container average, to spot skew.</li>
<li><strong>429 counts:</strong> a steady stream means a hot partition or too little throughput.</li>
<li><strong>Provisioned RU/s:</strong> match it to the real shape of your workload. This is the main lever for Cosmos DB cost optimization.</li>
</ul>
<h3>How do you migrate to a new partition key?</h3>
<p>You can&#8217;t change the partition key of an existing container in place. Create a new container with the right key, copy the data (e.g. with change feed or a bulk copy job) and cut over. Test the whole process on a copy first.</p>
<h3>Which recent features need a status check?</h3>
<p>Build 2026 (June 2026) announced the Linux Emulator and the All Versions and Deletes change feed mode as generally available, along with Global Secondary Indexes. Verify each feature&#8217;s status in the announcement before you depend on it.</p>
<h3>Production readiness checklist</h3>
<p>Work through these steps in order before go-live:</p>
<ol>
<li><strong>Model access patterns:</strong> list your reads, writes and filters first.</li>
<li><strong>Choose and load-test the partition key:</strong> use realistic data and traffic.</li>
<li><strong>Confirm 20 GB headroom:</strong> check that no logical partition value can outgrow the limit.</li>
<li><strong>Colocate entities:</strong> group related types in one container where queries and transactions need it.</li>
<li><strong>Trim the indexing policy:</strong> exclude unused paths.</li>
<li><strong>Log RequestCharge:</strong> record it for every operation.</li>
<li><strong>Use a singleton CosmosClient:</strong> create one client per application and reuse it for all requests, instead of creating a new client per call. The client holds the connections and the retry settings, so sharing it keeps behavior consistent.</li>
<li><strong>Set SDK retry options:</strong> configure the retry count and maximum wait on that single client.</li>
<li><strong>Consider preferred regions and direct mode (optional):</strong> if your account has more than one region, set the preferred regions list so the SDK reads from the closest one. Also review the SDK&#8217;s connection mode (direct or gateway) for your workload. Check the current SDK documentation for the exact options.</li>
<li><strong>Alert on 429s and partition skew:</strong> watch both.</li>
<li><strong>Relax consistency per request where staleness is acceptable:</strong> set the account default to the weakest level your correctness rules allow. Then relax individual reads that tolerate stale data, for example a product-review feed. A request can only use a level equal to or weaker than the default, so confirm this rule on the official consistency-levels page.</li>
<li><strong>Rehearse migration and restore:</strong> prove you can recover before you must.</li>
</ol>
<h2 id="key-takeaways">Key takeaways</h2>
<p>These are the points to remember from this guide, in the order you will likely need them when designing or fixing a Cosmos DB workload:</p>
<ul>
<li><strong>Choose the partition key from access patterns:</strong> pick a key with high cardinality that spreads both storage and RU use evenly and appears in your most common query filters. Keep each logical partition well under the 20 GB limit.</li>
<li><strong>Use hierarchical partition keys for multi-tenant workloads:</strong> you can use up to three levels (e.g. TenantId, UserId, id), and every level should have high cardinality. Ending the key with a unique value such as /id lets the earlier levels grow past 20 GB.</li>
<li><strong>Fix hot partitions with the data model, not more RU/s:</strong> throughput is divided across physical partitions, so raising it does not help a skewed key. Change the key, add a spreading level, or split read and write shapes with change feed.</li>
<li><strong>Prefer point reads and project only what you need:</strong> read by id plus partition key when you can, keep queries inside one partition, and select specific properties instead of <code>SELECT *</code>. Exclude paths you never filter or sort on from the indexing policy to lower write cost.</li>
<li><strong>Measure RequestCharge and handle 429s deliberately:</strong> <a href="https://thecodebuzz.com/logging-best-practices-and-guidelines-software-development/">log the RU charge</a> for each important operation, set the SDK retry options, and alert on throttling and partition skew. A 429 caused by a hot partition needs a key fix, not a bigger RU/s setting.</li>
<li><strong>Treat the account default consistency as the ceiling:</strong> Session is the default, so pick the weakest level your correctness rules allow. A request can only relax that level, never make it stronger. If a critical read (e.g. a banking balance check) needs stronger guarantees, set the account default to that stronger level and relax it per request for the reads that can tolerate it. Confirm these rules on the official consistency-levels page.</li>
<li><strong>Decide the partition key early:</strong> you cannot change it in place on an existing container. A change means creating a new container, copying the data and cutting over, so rehearse that migration on a copy first.</li>
</ul>
<p>That&#8217;s all! Happy coding!</p>
<p>Did this guide solve your Cosmos DB partitioning, RU or consistency issue? Please share your questions or experience in the comments below.</p>
<p>Bookmark this page or share it with your team, so it is handy for your next design review. Subscribe to TheCodeBuzz for more database best-practice guides.</p>
<h2 id="faq">FAQ</h2>
<h3><strong>How do I choose a good partition key in Cosmos DB?</strong></h3>
<p>Choose a key with <strong>high cardinality</strong> that spreads both storage and RU use evenly and appears in your most common query filters. Keep each logical partition well under the 20 GB limit, and avoid keys that send all current writes to one value, such as a purely time-based key. If you need more than one level, a hierarchical key of up to three levels (e.g. TenantId, UserId, id) works well for multi-tenant workloads. Decide early, because the key of an existing container cannot be changed in place.</p>
<h3><strong>What is a hot partition in Cosmos DB and how do I fix it?</strong></h3>
<p>A hot partition is one partition that takes far more RU or storage than the others, so it throttles while the rest of the container sits idle. To detect it, compare normalized RU consumption per partition in Azure Monitor. A few partitions near 100% while the container average stays low points to skew. Fix it with the data model: pick a higher-cardinality or hierarchical key, add a spreading level such as a time bucket, or split read and write shapes. Raising RU/s does not help, because throughput is divided across partitions.</p>
<h3><strong>How do I reduce RU consumption in Cosmos DB?</strong></h3>
<p>Use point reads by id and partition key instead of queries, keep queries inside a single partition, and select only the properties you need instead of <code>SELECT *</code>. Exclude paths you never filter or sort on from the indexing policy, since every indexed path is maintained on each write. Then measure the <code>RequestCharge</code> of each operation, because the same query on the same data always costs the same RUs. Check your own workload rather than relying on generic savings figures.</p>
<h3><strong>How do I fix 429 Request Rate Too Large errors in Cosmos DB?</strong></h3>
<p>First check whether the 429s come from a hot partition. If they do, fix the partition key or data model, because adding RU/s will not solve skew. If load is spread evenly, tune the SDK retry settings (<code>MaxRetryAttemptsOnRateLimitedRequests</code> and <code>MaxRetryWaitTimeOnRateLimitedRequests</code>), which honor the <code>x-ms-retry-after-ms</code> header the server returns. If throttling stays steady after that, raise throughput to match the real workload shape. Workloads that are not latency-sensitive can run with less throughput and rely on rate limiting.</p>
<h3><strong>Which Cosmos DB consistency level should I use, and what is the default?</strong></h3>
<p><strong>Session</strong> is the default level, but it is not the right choice for every workload. Pick the weakest level your correctness rules allow, since stronger levels cost more in latency and availability.</p>
<p>The account default is the strongest level any request can get. A request can only weaken it with a per-request override, never strengthen it. So if even one operation, such as a banking balance check, needs a strong guarantee, the account default must be at least that strong. Then relax the other, less critical reads (e.g. a product-review feed) per request.</p>
<p>Confirm on the official consistency-levels page whether Strong is available with multi-region writes, because that detail comes from a third-party source.</p>
<h2 id="sources">Sources and further reading</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys" target="_blank" rel="noopener">Hierarchical Partition Keys &#8211; Azure Cosmos DB</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys-unlimited-scale" target="_blank" rel="noopener">Unlimited logical partition storage with hierarchical partition keys &#8211; Azure Cosmos DB</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/optimize-cost-reads-writes" target="_blank" rel="noopener">Optimize Request Cost &#8211; Azure Cosmos DB</a> (Microsoft Learn)</li>
</ul>
<p>&nbsp;</p><p>The post <a href="https://thecodebuzz.com/cosmos-db-best-practices-partitioning/">Cosmos DB Best Practices: Partitioning, RU and Consistency</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/cosmos-db-best-practices-partitioning/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Neo4j Best Practices: Graph Modeling and Cypher Performance</title>
		<link>https://thecodebuzz.com/neo4j-best-practices-graph-modeling-cypher/</link>
					<comments>https://thecodebuzz.com/neo4j-best-practices-graph-modeling-cypher/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Tue, 10 Nov 2026 04:45:19 +0000</pubDate>
				<category><![CDATA[Database Best Practices]]></category>
		<category><![CDATA[Cypher]]></category>
		<category><![CDATA[Data Modeling]]></category>
		<category><![CDATA[Graph Database]]></category>
		<category><![CDATA[Neo4j]]></category>
		<category><![CDATA[Performance Tuning]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=31064</guid>

					<description><![CDATA[<p>Neo4j best practices for graph modeling, indexes, MERGE, PROFILE tuning, batching, Cypher 25 and production operations, with examples you can apply.</p>
<p>The post <a href="https://thecodebuzz.com/neo4j-best-practices-graph-modeling-cypher/">Neo4j Best Practices: Graph Modeling and Cypher Performance</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>This is a practical Neo4j best practices guide covering <a href="https://thecodebuzz.com/what-is-data-modelling-fundamentals-sql-nosql-guidelines/">graph data modeling</a>, Cypher query optimization and production operations. It&#8217;s written for software engineers, architects and DBAs who run Neo4j, a graph database, or plan to. Every rule comes with its reason, so you can decide when it applies to your workload.</p>
<p>The scope is modeling, supernodes, indexes and constraints, reading plans with EXPLAIN and PROFILE, batched writes, Cypher 25 vs Cypher 5, operations, common anti-patterns and a production readiness checklist. The model is query-driven. You write down your important queries first and let them shape the graph. A graph isn&#8217;t always the right choice, and we will say when a <a href="https://thecodebuzz.com/postgresql-best-practices-production/">relational database is simpler</a>.</p>
<p>Neo4j ships roughly monthly in calendar-versioned releases, and the 5.26 LTS line is still patched. Many older guides ignore Cypher 25, the default language setting and concurrent batching, so this one covers them. Check the deployment center for the latest patch before you install.</p>
<blockquote><p><strong>Version note:</strong> This guide targets Neo4j 2026.10.2 and Neo4j 5.26 LTS (5.26.33), both released on 6 November 2026, with Cypher 25 and Cypher 5.</p>
<p><strong>In this guide</strong></p></blockquote>
<ul>
<li><a href="#neo4j-best-practices-at-a-glance">Neo4j Best Practices at a Glance</a></li>
<li><a href="#neo4j-graph-data-modeling-nodes-relationships-properties-and">Neo4j Graph Data Modeling: Nodes, Relationships, Properties and Supernodes</a></li>
<li><a href="#neo4j-indexes-constraints-and-cypher-query-optimization-with">Neo4j Indexes, Constraints and Cypher Query Optimization with EXPLAIN and PROFILE</a></li>
<li><a href="#batching-writes-and-choosing-between-cypher-25-and-cypher-5">Batching Writes and Choosing Between Cypher 25 and Cypher 5</a></li>
<li><a href="#neo4j-performance-tuning-and-operations-memory-monitoring-ba">Neo4j Performance Tuning and Operations: Memory, Monitoring, Backups and Upgrades</a></li>
<li><a href="#common-anti-patterns-and-production-readiness-checklist">Common Anti-Patterns and Production Readiness Checklist</a></li>
<li><a href="#key-takeaways">Key takeaways</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="neo4j-best-practices-at-a-glance">Neo4j Best Practices at a Glance</h2>
<p>Neo4j is a graph database that stores data as nodes and relationships and is queried with Cypher. This guide covers Neo4j graph data modeling, Cypher query optimization, Neo4j performance tuning and operations, so you can run it safely in production.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note: </strong> Think of a subway map versus a spreadsheet. A spreadsheet lists stations in rows, and finding a route means matching rows again and again. A subway map stores the connections directly, so you just follow the lines. A graph database works like the map: the links between things are stored, not worked out at query time.</p></blockquote>
<h3>Best practices summary table</h3>
<p>The table below shows what to do and what to avoid in each area. Later sections explain the reasoning.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Area</th>
<th>Do this</th>
<th>Avoid this</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Data modeling</strong></td>
<td>Write the key queries first, use specific relationship types (e.g. <code>PLACED</code>), test on sample data</td>
<td>A generic <code>CONNECTED_TO</code> type, a model never tested on real data</td>
</tr>
<tr>
<td><strong>Supernodes</strong></td>
<td>Split by type or time, add intermediate nodes</td>
<td>One hub node with millions of relationships</td>
</tr>
<tr>
<td><strong>Indexes and constraints</strong></td>
<td>Uniqueness constraint on the lookup key</td>
<td>Label scans on large labels</td>
</tr>
<tr>
<td><strong>Queries</strong></td>
<td>Parameters, early filtering, return only needed properties, capped variable-length paths</td>
<td>Literals, whole-node returns, unbounded paths</td>
</tr>
<tr>
<td><strong>Writes</strong></td>
<td><code>MERGE</code> on a constrained key, batched <code>CALL { } IN TRANSACTIONS</code></td>
<td>One huge transaction</td>
</tr>
<tr>
<td><strong>Cypher version</strong></td>
<td>Pick the version deliberately per database</td>
<td>Inheriting a default by accident</td>
</tr>
<tr>
<td><strong>Operations</strong></td>
<td>Pin patch versions, test upgrades, back up and test restores</td>
<td>Untested upgrades</td>
</tr>
</tbody>
</table>
</figure>
<h3>Which Neo4j versions are current?</h3>
<p>Neo4j 2026.10.2 and 5.26.33 are the current builds, both released on 6 November 2026. Check the deployment center for the latest patch before you install.</p>
<h2 id="neo4j-graph-data-modeling-nodes-relationships-properties-and">Neo4j Graph Data Modeling: Nodes, Relationships, Properties and Supernodes</h2>
<p>Write your most important queries first, let them decide what becomes a node, a relationship or a property, and then test the model on a loaded sample of real data. Neo4j&#8217;s own modeling guidance says there is no single right model, and the model can change as your priorities change. Good Neo4j graph data modeling is therefore iterative, not a one-time design.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note: </strong> In a graph, a relationship is stored as a direct link between two nodes. Think of a subway map: you follow the lines from station to station. A spreadsheet is more like a timetable, where you search rows to find a match. The analogy stops fitting when a station has thousands of lines, which is the supernode problem covered below.</p></blockquote>
<h3>How do I decide between a node, a relationship and a property?</h3>
<p>Use the questions in the table below. The animated diagram that follows walks through the same decisions step by step.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Data</th>
<th>Model as</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Entity you look up or connect to</strong></td>
<td>Node</td>
<td>Queries can start there and traverse outward.</td>
</tr>
<tr>
<td><strong>Verb between two entities</strong></td>
<td>Relationship with a specific type (e.g. <code>PLACED</code>, <code>CONTAINS</code>)</td>
<td>A specific type lets a query follow only the links it needs. A generic <code>CONNECTED_TO</code> forces filtering afterwards.</td>
</tr>
<tr>
<td><strong>Value read after reaching the entity</strong></td>
<td>Property</td>
<td>It adds no extra hops and keeps the graph small.</td>
</tr>
<tr>
<td><strong>Rich or multi-party connection</strong></td>
<td>Intermediate node</td>
<td>It can hold data, join more than two entities and be indexed.</td>
</tr>
</tbody>
</table>
</figure>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/11/neo4j-best-practices-graph-modeling-cypher-diagram-2.gif" alt="Decision tree: data becomes a node, property, intermediate node or typed relationship, then a hub is split into monthly buckets." width="1200" height="675" /><figcaption>Decide node, property, relationship or intermediate node, then bucket any dense hub by month to avoid a supernode.</figcaption></figure>
<p>Keep the label count sensible. Add a new label when queries really need to tell the entities apart, not for every small variation.</p>
<h3>When should a relationship become an intermediate node?</h3>
<p>Use an intermediate node when the connection has rich data, joins more than two things, or must itself be connected or indexed. <strong>Example:</strong> in retail, a <code>Purchase</code> node can link a <code>Customer</code>, a <code>Product</code> and a <code>Store</code>, and carry the price, quantity and timestamp. A single relationship cannot join three entities.</p>
<h3>How do I model date-range queries?</h3>
<p>If queries must return results <a href="https://thecodebuzz.com/neo4j-graph-db-date-range-query-examples/">within a date range</a>, consider a separate date node or relationship instead of a node property. The query then traverses to a small set of date nodes and follows their links, instead of testing a date property on a large number of candidate nodes.</p>
<h3>How do I avoid supernodes?</h3>
<p>A supernode is a dense node with a very large number of relationships. Traversing it is slow because the expansion has to touch every relationship it holds. Use these patterns:</p>
<ul>
<li><strong>Split by relationship type:</strong> use distinct types instead of one type for everything.</li>
<li><strong>Bucket by time:</strong> <code>Account -&gt; Month -&gt; Transaction</code>.</li>
<li><strong>Add category nodes:</strong> put an intermediate node between the hub and its many neighbors.</li>
<li><strong>Filter on type and direction:</strong> name both in the pattern so the query ignores relationships it doesn&#8217;t need.</li>
</ul>
<p><strong>Example:</strong> a finance <code>Account</code> with millions of transactions is remodeled so each transaction hangs off a monthly bucket node. With one bucket per month, an account open for three years would hold about 36 <code>HAS_MONTH</code> relationships (an illustrative figure, your numbers will differ), and each month holds only its own transactions.</p>
<h3>Which constraints does the bucketed model need?</h3>
<p>Create the constraints before loading data, so every <code>MERGE</code> and <code>MATCH</code> in the model starts with an index seek and not a label scan. Each constraint creates a backing index on its key. Run these statements once, as separate commands:</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-cypher">CREATE CONSTRAINT account_id_unique IF NOT EXISTS
FOR (a:Account) REQUIRE a.accountId IS UNIQUE;

CREATE CONSTRAINT month_key_unique IF NOT EXISTS
FOR (m:Month) REQUIRE (m.accountId, m.month) IS UNIQUE;

CREATE CONSTRAINT transaction_id_unique IF NOT EXISTS
FOR (t:Transaction) REQUIRE t.txId IS UNIQUE;</code></pre>
<p>The composite constraint on <code>Month</code> makes the pair <code>accountId</code> and <code>month</code> the lookup key. A node key constraint on the same two properties also works if your edition supports it.</p>
<h3>How do I create and query the monthly buckets?</h3>
<p>Merge each node on its constrained key, then connect them, so a repeated load does not create duplicates. The query side starts at one account, so it is anchored on an indexed property.</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-cypher">// Create: account, monthly bucket and transaction
MERGE (a:Account {accountId: $accountId})
MERGE (m:Month {accountId: $accountId, month: $month})
MERGE (a)-[:HAS_MONTH]-&gt;(m)
CREATE (m)-[:CONTAINS]-&gt;(:Transaction {txId: $txId, amount: $amount, postedAt: datetime($postedAt)});

// Query: one month of transactions for one account
MATCH (:Account {accountId: $accountId})-[:HAS_MONTH]-&gt;(:Month {month: $month})-[:CONTAINS]-&gt;(t:Transaction)
RETURN t.txId, t.amount, t.postedAt
ORDER BY t.postedAt;</code></pre>
<p>The query finds one account through its constraint index, jumps to one month and reads only that month&#8217;s transactions. Run <code>PROFILE</code> on it with sample data to confirm the plan shows index seeks and no <code>NodeByLabelScan</code>.</p>
<h3>When is a graph the wrong choice?</h3>
<p>Use a relational database when the work is mostly aggregations over flat tables, bulk analytics scans, or data with few relationships. A graph pays off when queries follow connections, and it adds modeling effort that flat data does not repay.</p>
<h2 id="neo4j-indexes-constraints-and-cypher-query-optimization-with">Neo4j Indexes, Constraints and Cypher Query Optimization with EXPLAIN and PROFILE</h2>
<p>The fastest Cypher queries start at specific nodes found through an index or constraint. Anchor every query on an indexed property, then check the plan with <strong>EXPLAIN</strong> (shows the plan, executes nothing) and <strong>PROFILE</strong> (runs the query and shows rows and db hits).</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note: </strong> An index works like the index at the back of a book: you look up a word and jump to the right page. A label scan is reading every page to find it. The analogy stops at writes, because Neo4j also has to update the index whenever data changes.</p></blockquote>
<h3>Which constraints and indexes should you create?</h3>
<p>Create a uniqueness constraint on the natural key (e.g. a customer email or order number). It also creates the backing index, so lookups on that key become index seeks. Add range or text indexes for properties you filter on often.</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">CREATE CONSTRAINT customer_email_unique IF NOT EXISTS
FOR (c:Customer) REQUIRE c.email IS UNIQUE;

CREATE INDEX order_date_range IF NOT EXISTS
FOR (o:Order) ON (o.orderDate);

CREATE TEXT INDEX product_name_text IF NOT EXISTS
FOR (p:Product) ON (p.name);</code></pre>
<p>The first statement enforces uniqueness, the second speeds up date filters, and the third supports string matching on product names.</p>
<p>The constraint matters most for <code>MERGE</code>, which means &#8220;match this pattern or create it&#8221;. Without a constraint, <code>MERGE</code> may scan the label, and two concurrent transactions can both find nothing and both create the node. With the constraint, the lookup is an index seek and duplicates are rejected. The 2025 changelog added the MergeUniqueNode and MergeInto operators, so <code>MERGE</code> on a constrained key is cheaper than it used to be.</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">MERGE (c:Customer {email: $email})
ON CREATE SET c.name = $name, c.createdAt = datetime()
ON MATCH SET c.lastSeen = datetime()
RETURN c.email;</code></pre>
<p>The email and name come in as parameters, and the constrained key keeps the merge safe and fast.</p>
<h3>How do you read a PROFILE plan?</h3>
<p>Read the plan from the bottom up and look at the leaf operators first. <strong>Db hits</strong> are the units of storage work an operator does. <strong>Cardinality</strong> is the number of rows an operator produces (estimated in EXPLAIN, actual in PROFILE).</p>
<p>The animated diagram below shows the tuning loop: run EXPLAIN, run PROFILE, match the warning sign to a fix, and repeat until the plan shows an index seek.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/11/neo4j-best-practices-graph-modeling-cypher-diagram-3.gif" alt="Tuning loop: slow query, EXPLAIN, PROFILE, four plan warnings each with a fix, back to PROFILE again, ending at an index seek." width="1200" height="675" /><figcaption>Match each plan warning to its fix, then PROFILE again until the leaf operator is an index seek with low db hits.</figcaption></figure>
<p>This table lists the operators you will see most often and what to do about each one:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Operator</th>
<th>What it means</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>NodeIndexSeek / NodeUniqueIndexSeek</strong></td>
<td>Starts at specific nodes</td>
<td>Good, keep it</td>
</tr>
<tr>
<td><strong>NodeIndexScan</strong></td>
<td>Reads an index, no value lookup</td>
<td>Acceptable</td>
</tr>
<tr>
<td><strong>NodeByLabelScan</strong></td>
<td>Reads every node with the label</td>
<td>Add index or constraint</td>
</tr>
<tr>
<td><strong>CartesianProduct</strong></td>
<td>Pairs every row with every row</td>
<td>Connect the patterns</td>
</tr>
<tr>
<td><strong>Eager</strong></td>
<td>Loads all rows before continuing</td>
<td>Reorder reads and writes, or batch</td>
</tr>
</tbody>
</table>
</figure>
<p>A large gap between estimated and actual rows is another warning, because the planner chose its plan on wrong numbers. PROFILE uses extra resources, so use it while tuning, not as a habit on every production query.</p>
<p>Here is a retail example. Before the constraint exists, the lookup is a label scan:</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">PROFILE
MATCH (c:Customer {email: $email})-[:PLACED]-&gt;(o:Order)
RETURN o.orderId, o.total;

// Before: NodeByLabelScan on Customer, then Filter on email
// Fix:
CREATE CONSTRAINT customer_email_unique IF NOT EXISTS
FOR (c:Customer) REQUIRE c.email IS UNIQUE;
// After: NodeUniqueIndexSeek on Customer, far fewer db hits</code></pre>
<p>The leaf operator changed from <code>NodeByLabelScan</code> to <code>NodeUniqueIndexSeek</code>, so the query touches one customer instead of all of them.</p>
<h3>Which query rules improve Cypher performance?</h3>
<p>These rules cover most Cypher query optimization work, and each one has a reason:</p>
<ul>
<li><strong>Use parameters:</strong> Neo4j can auto-parameterize some simple literals, but that does not cover every query. Explicit parameters are the reliable way to get plan reuse, because the cached plan is shared across all values.</li>
<li><strong>Filter early:</strong> Fewer rows early means less work in every later operator.</li>
<li><strong>Aggregate early:</strong> Collapse rows before further matching or joining.</li>
<li><strong>Return properties:</strong> <code>RETURN o.orderId, o.total</code> moves less data than returning whole nodes.</li>
<li><strong>Cap variable-length paths:</strong> Write <code>[:KNOWS*1..4]</code>, because an unbounded path can explore most of the graph.</li>
<li><strong>Be explicit:</strong> Give labels and relationship types, including on both ends of a relationship, so the planner has better statistics.</li>
<li><strong>Avoid Cartesian products:</strong> Two disconnected <code>MATCH</code> patterns multiply their rows. Connect them or split the query.</li>
<li><strong>Limit OPTIONAL MATCH chains:</strong> Each one keeps rows alive that a plain <code>MATCH</code> would drop.</li>
</ul>
<h3>How do you run a parameterized query from Python?</h3>
<p>Pass values as keyword arguments to <code>execute_query</code> and never build the query string by hand. This keeps plans reusable and blocks Cypher injection.</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">from neo4j import GraphDatabase, RoutingControl

URI = "neo4j://localhost:7687"
AUTH = ("shop_app", "change-me")

with GraphDatabase.driver(URI, auth=AUTH) as driver:
    records, summary, keys = driver.execute_query(
        "MATCH (c:Customer {email: $email})-[:PLACED]-&gt;(o:Order) "
        "RETURN o.orderId AS orderId, o.total AS total",
        email="anita@example.com",
        database_="neo4j",
        routing_=RoutingControl.READ,
    )
    for record in records:
        print(record["orderId"], record["total"])</code></pre>
<p>The <code>email</code> value travels as a parameter, and <code>routing_=RoutingControl.READ</code> sends the read to a suitable cluster member. The <code>shop_app</code> user is a placeholder account name, so assign it a least-privilege role such as the read-only role described in the security section.</p>
<h2 id="batching-writes-and-choosing-between-cypher-25-and-cypher-5">Batching Writes and Choosing Between Cypher 25 and Cypher 5</h2>
<p>Load data with <code>CALL { } IN TRANSACTIONS</code> instead of one huge transaction. Memory stays bounded and a failure only affects one batch. A <strong>transaction batch</strong> is a fixed number of input rows that are committed together as one small transaction.</p>
<h3>How do you load data in batches?</h3>
<p>Create the uniqueness constraint first, then read the rows and commit every 10,000 of them in their own transaction. <code>MERGE</code> on a constrained key means a re-run does not create duplicates, and the constraint is what enforces that.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note: </strong> Think of moving house. One trip with every box will overload the van, but many trips with a few boxes each are safe, and a flat tyre only costs you one trip. Unlike boxes, a committed batch stays in the database even if a later batch fails.</p></blockquote>
<p>This statement creates the constraint on the lookup key. Run it once, before the load, as its own statement:</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">CREATE CONSTRAINT order_id_unique IF NOT EXISTS
FOR (o:Order) REQUIRE o.orderId IS UNIQUE;</code></pre>
<p>The constraint also creates a backing index, so each <code>MERGE</code> looks up the order by <code>orderId</code> instead of scanning the <code>Order</code> label. <code>IF NOT EXISTS</code> makes the statement safe to repeat in deployment scripts.</p>
<p>This statement loads the orders in batches of 10,000 rows:</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">LOAD CSV WITH HEADERS FROM 'file:///orders.csv' AS row
CALL (row) {
  MERGE (o:Order {orderId: row.orderId})
  SET o.total = toFloat(row.total)
} IN TRANSACTIONS OF 10000 ROWS;</code></pre>
<p>The same pattern works with <code>UNWIND $rows AS row</code> for a parameter list. This statement must run in an implicit (auto-commit) transaction, so in Neo4j Browser prefix it with <code>:auto</code> and do not wrap it in <code>BEGIN</code>.</p>
<h3>Can you run batches concurrently?</h3>
<p><code>IN CONCURRENT TRANSACTIONS</code> runs several batches in parallel and speeds up large loads. It can also <strong>deadlock</strong> when two batches try to lock the same node, for example two orders for the same customer.</p>
<p>The animated diagram below shows batches entering three lanes, a deadlock risk appearing, and <code>DISJOINT BY</code> routing the conflicting batches into separate lanes.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/11/neo4j-best-practices-graph-modeling-cypher-diagram-4.gif" alt="Input rows are split into batches for three worker lanes; two lanes contend for one node, DISJOINT BY separates them, and each lane commits to the database." width="1200" height="675" /><figcaption>IN CONCURRENT TRANSACTIONS speeds up loads, and DISJOINT BY (experimental, Cypher 25) keeps conflicting batches from running together.</figcaption></figure>
<p>Cypher 25 adds <code>DISJOINT BY</code> to schedule batches so conflicting ones do not run together. The 2026 changelog marks it <strong>experimental</strong>, so test it on your own data before using it in production. Check the Cypher Manual for your exact server version for the current syntax, because experimental features can change.</p>
<p>A database-wide default batch strategy can be set with <code>dbms.cypher.transactions.default_subquery_batch_strategy</code>.</p>
<h3>Cypher 25 vs Cypher 5: which should you use?</h3>
<p>Use Cypher 25 for new work unless you depend on Cypher 5 features that it removed. The table compares the two languages.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Aspect</th>
<th>Cypher 5</th>
<th>Cypher 25</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Status</strong></td>
<td>Frozen</td>
<td>Evolving, gains features with each server release</td>
</tr>
<tr>
<td><strong>Changes allowed</strong></td>
<td>Bug fixes and possible performance work</td>
<td>Additive only since it first appeared in server release 2025.06</td>
</tr>
<tr>
<td><strong>Origin</strong></td>
<td>The earlier language line</td>
<td>Started as a clone of Cypher 5</td>
</tr>
<tr>
<td><strong>Additions</strong></td>
<td>None</td>
<td><code>REPEATABLE ELEMENTS</code>, <code>NEXT</code>, <code>WHEN ... THEN</code> subqueries, native full-text <code>SEARCH</code> and Path Modes (2026)</td>
</tr>
<tr>
<td><strong>Removals</strong></td>
<td>None</td>
<td>Some deprecated Cypher 5 features</td>
</tr>
</tbody>
</table>
</figure>
<h3>How do you set the Cypher version?</h3>
<p>Set a default per database, and override it per query with a version prefix. This lets you migrate one database or one query at a time.</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">ALTER DATABASE sales SET DEFAULT LANGUAGE CYPHER 25;

CYPHER 5
MATCH (c:Customer {customerId: $id}) RETURN c.name;

CYPHER 25
MATCH (c:Customer {customerId: $id}) RETURN c.name;</code></pre>
<p>The first statement changes the default for the <code>sales</code> database. The prefixed queries run in the named version regardless of that default.</p>
<blockquote><p><strong>Warning: </strong> Since 2026.02 the <code>neo4j.conf</code> shipped with the server sets <code>db.query.default_language=CYPHER_25</code>. If you reuse a config file across upgrades or new servers, check this setting, because it decides which language new databases default to.</p></blockquote>
<h2 id="neo4j-performance-tuning-and-operations-memory-monitoring-ba">Neo4j Performance Tuning and Operations: Memory, Monitoring, Backups and Upgrades</h2>
<p>Size the <strong>page cache and heap separately</strong>, log slow queries, back up and rehearse restores, and treat every upgrade as a tested change. The monthly releases have shipped regressions, so none of this is optional for production.</p>
<h3>How should you size memory and storage?</h3>
<p>Neo4j uses two memory areas for different jobs. The page cache holds graph data and indexes, and the heap holds query execution state.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note: </strong> Think of the page cache as a bookshelf next to your desk that keeps the books you read most often. The heap is the scratch paper for the question you are answering right now. A bigger bookshelf won&#8217;t help if your scratch paper keeps overflowing.</p></blockquote>
<p>A larger page cache does not fix a query that builds huge intermediate results, because that pressure lands on the heap. Fix the query first (see the PROFILE guidance earlier in this article). Run the <code>neo4j-admin server memory-recommendation</code> command for a starting estimate, then adjust from monitoring data. Keep data files and transaction logs on fast SSD storage.</p>
<h3>What should you monitor?</h3>
<p><a href="https://thecodebuzz.com/logging-best-practices-and-guidelines-software-development/">Turn on query logging</a> first, so slow queries are recorded instead of guessed at. Here is a simple example for <code>neo4j.conf</code> that logs only queries slower than one second:</p>
<p class="code-label"><strong>neo4j.conf</strong></p>
<pre><code class="language-text">db.logs.query.enabled=INFO
db.logs.query.threshold=1s</code></pre>
<p>At the <code>INFO</code> level, the threshold decides which queries are written to the log, so only queries that run longer than one second appear. Do not use <code>VERBOSE</code> for this purpose. It logs the start and end of every query and the threshold only applies at <code>INFO</code>, so the log grows quickly and the one-second filter has no effect.</p>
<p>Beyond the query log, track these:</p>
<ul>
<li><strong>Page cache hit ratio:</strong> a falling ratio means the working set no longer fits in memory.</li>
<li><strong>Heap and GC:</strong> long or frequent garbage collection pauses point to queries holding too much state.</li>
<li><strong>Transaction counts:</strong> watch open, committed and rolled-back transactions for spikes.</li>
</ul>
<p>Run <code>PROFILE</code> only on the worst offenders from the log, since it adds overhead.</p>
<h3>How do you secure a Neo4j deployment?</h3>
<p>Give the application a least-privilege user through role-based access control, and encrypt traffic with TLS for both Bolt and HTTP. Always pass values as parameters, which also prevents Cypher injection. The example below creates a read-only role for a reporting service:</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">CREATE ROLE app_reader IF NOT EXISTS;
GRANT ACCESS ON DATABASE neo4j TO app_reader;
GRANT MATCH {*} ON GRAPH neo4j TO app_reader;
CREATE USER report_svc IF NOT EXISTS SET PASSWORD $password CHANGE NOT REQUIRED;
GRANT ROLE app_reader TO report_svc;</code></pre>
<p>The service can now read the graph but cannot write or change the schema.</p>
<h3>How do you back up and restore safely?</h3>
<p>Take regular full and differential backups, then restore them into a separate environment on a schedule. <strong>A backup you have never restored is unproven.</strong> This command lets Neo4j choose between a full and a differential backup:</p>
<p class="code-label"><strong>Shell</strong></p>
<pre><code class="language-bash">neo4j-admin database backup --from=db-host:6362 --type=AUTO --to-path=/backups/neo4j neo4j</code></pre>
<p>If you use Ops Manager, version 1.15 and later (1.15.6 is current) needs a persistence database on Neo4j 5.26 LTS or newer.</p>
<h3>What does clustering give you?</h3>
<p>A cluster gives you availability and read scaling. Reads can be served by several members, while writes for a database go to its leader, which is one of the primary members for that database. A cluster does not replace backups.</p>
<h3>How should you handle upgrades?</h3>
<p>Pin to exact patch versions and test every upgrade in staging. The release notes record that 2026.07.0 could fail unexpectedly when using <code>trim()</code>, and 2026.08.0 could make some queries fail. Patch releases fixed both.</p>
<p>For clusters, read the upgrade notes first, then do a rolling upgrade one member at a time. Current builds are 5.26.33 and 2026.10.2, both released 6 November 2026. The table compares the two tracks:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Aspect</th>
<th>5.26 LTS</th>
<th>Calendar-versioned series</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Cadence</strong></td>
<td>Regular patches on a stable line</td>
<td>Roughly monthly releases</td>
</tr>
<tr>
<td><strong>Suits</strong></td>
<td>Teams that want few changes</td>
<td>Teams that want new features early</td>
</tr>
<tr>
<td><strong>Trade-off</strong></td>
<td>New features arrive later</td>
<td>More upgrades to test, with regressions seen in some releases</td>
</tr>
</tbody>
</table>
</figure>
<h2 id="common-anti-patterns-and-production-readiness-checklist">Common Anti-Patterns and Production Readiness Checklist</h2>
<p>Most Neo4j problems in production trace back to a short list of repeated mistakes. The table below shows each one, how it appears, and how to fix it.</p>
<h3>Which Neo4j anti-patterns cause the most trouble?</h3>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Anti-pattern</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Generic relationship type</strong></td>
<td>Patterns need property filters to tell edges apart</td>
<td>Use specific types (e.g. <code>PLACED</code>, <code>CONTAINS</code>)</td>
</tr>
<tr>
<td><strong>Supernode hub</strong></td>
<td>Traversals through one node get slower as it grows</td>
<td>Split by type, bucket by time, add intermediate nodes</td>
</tr>
<tr>
<td><strong>No constraint on MERGE key</strong></td>
<td>Label scans, duplicates under concurrency</td>
<td>Create a uniqueness constraint</td>
</tr>
<tr>
<td><strong>Literals instead of parameters</strong></td>
<td>Plans are not reused, injection risk</td>
<td>Pass values as parameters</td>
</tr>
<tr>
<td><strong>Cartesian product</strong></td>
<td><code>CartesianProduct</code> in the plan, row counts explode</td>
<td>Connect the patterns or run them in separate steps</td>
</tr>
<tr>
<td><strong>Unbounded variable-length path</strong></td>
<td>Memory spikes, queries that never finish</td>
<td>Cap it, e.g. <code>[:KNOWS*1..4]</code></td>
</tr>
<tr>
<td><strong>Returning whole nodes</strong></td>
<td>Large results, high db hits</td>
<td>Return only the properties you need</td>
</tr>
<tr>
<td><strong>One giant import transaction</strong></td>
<td>Heap pressure, long rollbacks</td>
<td>Use <code>CALL { } IN TRANSACTIONS</code></td>
</tr>
<tr>
<td><strong>Inherited default language</strong></td>
<td>New databases use a different Cypher version than expected</td>
<td>Set the default per database and check <code>neo4j.conf</code></td>
</tr>
<tr>
<td><strong>Untested monthly upgrade</strong></td>
<td>Queries fail after upgrade</td>
<td>Pin to patch releases and test in staging</td>
</tr>
</tbody>
</table>
</figure>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note: </strong> A Cartesian product pairs every row from one list with every row from another, like matching each shirt with each pair of trousers. Two lists of 1,000 rows give 1,000,000 combinations.</p></blockquote>
<h3>What should you verify before going live?</h3>
<p>Work through these steps in order and sign off each one.</p>
<ol>
<li><strong>Validate the model:</strong> run your top queries against a loaded sample of real data.</li>
<li><strong>Verify indexes and constraints:</strong> create them, then confirm with <code>PROFILE</code>.</li>
<li><strong>Use parameters:</strong> no literals in application queries.</li>
<li><strong>Cap variable-length paths:</strong> every <code>*</code> pattern gets an upper bound.</li>
<li><strong>Batch imports:</strong> no single huge write transaction.</li>
<li><strong>Set the Cypher version:</strong> choose it per database.</li>
<li><strong>Size memory and log queries:</strong> page cache and heap sized separately, query logging on.</li>
<li><strong>Test restores:</strong> schedule backups and rehearse a restore elsewhere.</li>
<li><strong>Review security:</strong> role-based access control and TLS.</li>
<li><strong>Pin and rehearse upgrades:</strong> lock to a patch release and run the upgrade in staging first.</li>
</ol>
<p>This snippet lists existing indexes and constraints so you can confirm steps 1 and 2:</p>
<p class="code-label"><strong>Cypher</strong></p>
<pre><code class="language-sql">SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state;
SHOW CONSTRAINTS YIELD name, type, labelsOrTypes, properties;</code></pre>
<p>Every index should show <code>ONLINE</code> as its state, and each MERGE key should appear in the constraints list.</p>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li><strong>Queries drive the model:</strong> Write your most important queries first, load a sample of real data, and test the model against them before committing. The model can change when priorities change.</li>
<li><strong>Use specific relationship types and intermediate nodes:</strong> Name relationships after the verb (e.g. <code>PLACED</code>, <code>CONTAINS</code>) instead of a generic <code>CONNECTED_TO</code>. When a connection carries rich data or links more than two things, make it a node.</li>
<li><strong>Split supernodes by type or time:</strong> Bucket by month (Account to Month to Transaction), add category nodes, and always filter on a specific relationship type and direction.</li>
<li><strong>Combine a constraint, parameters and MERGE:</strong> Create a uniqueness constraint on the lookup key, pass values as parameters, and MERGE on that key. This gives you an index-backed lookup, plan reuse and safe upserts.</li>
<li><strong>Read PROFILE for the warning signs:</strong> Look for <code>NodeByLabelScan</code> on large labels, <code>CartesianProduct</code>, <code>Eager</code> and a big gap between estimated and actual rows. Cap variable-length paths and return only the properties you need.</li>
<li><strong>Batch writes and treat DISJOINT BY as experimental:</strong> Use <code>CALL { } IN TRANSACTIONS</code> so memory stays bounded and failures stay small. Test <code>DISJOINT BY</code> thoroughly before relying on it in production.</li>
<li><strong>Choose the Cypher version deliberately:</strong> Set the default language per database and check your <code>neo4j.conf</code>, because the shipped file has defaulted to Cypher 25 since 2026.02. Use Cypher 25 for new work and migrate gradually.</li>
<li><strong>Pin patch releases and test restores:</strong> Monthly releases have shipped regressions (2026.07.0 and 2026.08.0), so rehearse upgrades in staging. A backup that has never been restored is unproven.</li>
</ul>
<p>That&#8217;s all! Happy coding!</p>
<p>Did this article solve your Neo4j modeling or Cypher performance problem? Please share your questions and experience in the comments below.</p>
<p>Bookmark this guide or share it with your team, and subscribe to TheCodeBuzz for more database best-practices articles.</p>
<h2 id="faq">FAQ</h2>
<h3><strong>How should I model data in Neo4j: node, relationship or property?</strong></h3>
<p>Use a node for an entity you look up or connect to, a relationship for a verb with a specific type (e.g. <code>PLACED</code>, <code>CONTAINS</code>), and a property for a value you only read after you reach the node. The exception is a connection that carries rich data, links more than two things, or must be indexed or connected itself. In that case, use an intermediate node, such as a Purchase node linking Customer, Product and Store. Write your important queries first and test the model on a loaded sample of real data.</p>
<h3><strong>How do I speed up a slow Cypher query?</strong></h3>
<p>Start by checking that the query is anchored on an indexed or constrained property, then work through the rest of this list:</p>
<ul>
<li><strong>Parameters:</strong> Use them instead of literals so plans can be reused.</li>
<li><strong>Early filtering:</strong> Filter and aggregate as early as possible.</li>
<li><strong>Return only properties:</strong> Return the properties you need, not whole nodes.</li>
<li><strong>Capped paths:</strong> Put an upper bound on variable-length patterns, e.g. <code>[:KNOWS*1..4]</code>.</li>
<li><strong>PROFILE:</strong> Run it to see which operator is doing the most work.</li>
</ul>
<h3><strong>What do EXPLAIN and PROFILE show, and which operators should I look for?</strong></h3>
<p><code>EXPLAIN</code> shows the query plan without running the query, so nothing executes. <code>PROFILE</code> runs the query and adds the actual rows and db hits for each operator. Node index seek and node index scan are the operators you want, because they start the query at specific nodes. Treat <code>NodeByLabelScan</code> on a large label, <code>CartesianProduct</code>, <code>Eager</code> and a big gap between estimated and actual rows as warning signs. Use <code>PROFILE</code> only while tuning, since it uses extra resources.</p>
<h3><strong>How do I avoid supernodes (dense nodes) in Neo4j?</strong></h3>
<p>Change the model so no single node collects millions of relationships. Split relationships by type, bucket them by time (e.g. Account to Month to Transaction), or add intermediate category nodes. In queries, filter on a specific relationship type and direction so the expansion does not touch every edge. A supernode is slow to traverse because the expansion has to go through all of its relationships.</p>
<h3><strong>What is the difference between Cypher 5 and Cypher 25, and which should I use?</strong></h3>
<p>Cypher 5 is frozen and receives only bug fixes and possible performance work. Cypher 25 started as a clone of Cypher 5 and gains features with each server release, but it has allowed only additive changes since it first appeared in server release 2025.06. Use Cypher 25 for new work unless you depend on Cypher 5 features that it removed, and migrate gradually. You can set the default per database with <code>ALTER DATABASE &lt;name&gt; SET DEFAULT LANGUAGE CYPHER 25</code> and override it per query with a version prefix.</p>
<h2 id="sources">Sources and further reading</h2>
<ul>
<li><a href="https://neo4j.com/deployment-center/" target="_blank" rel="noopener">Neo4j deployment center</a> (Neo4j)</li>
<li><a href="https://neo4j.com/blog/developer/cypher-versioning/" target="_blank" rel="noopener">Cypher versioning</a> (Neo4j)</li>
<li><a href="https://neo4j.com/docs/operations-manual/current/changes-2025-2026/" target="_blank" rel="noopener">Changes in Neo4j 2025-2026 series &#8211; Operations Manual</a> (Neo4j)</li>
</ul>
<p>&nbsp;</p><p>The post <a href="https://thecodebuzz.com/neo4j-best-practices-graph-modeling-cypher/">Neo4j Best Practices: Graph Modeling and Cypher Performance</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/neo4j-best-practices-graph-modeling-cypher/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>DynamoDB Best Practices: Single-Table Design, Keys and Cost</title>
		<link>https://thecodebuzz.com/dynamodb-best-practices-single-table-design/</link>
					<comments>https://thecodebuzz.com/dynamodb-best-practices-single-table-design/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 03:36:42 +0000</pubDate>
				<category><![CDATA[Database Best Practices]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cost Optimization]]></category>
		<category><![CDATA[Data Modeling]]></category>
		<category><![CDATA[DynamoDB]]></category>
		<category><![CDATA[NoSQL]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30969</guid>

					<description><![CDATA[<p>DynamoDB best practices for single-table design, partition key choice, hot partitions, GSIs and cost, with Python and C# examples and a checklist.</p>
<p>The post <a href="https://thecodebuzz.com/dynamodb-best-practices-single-table-design/">DynamoDB Best Practices: Single-Table Design, Keys and Cost</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Today in this article, we will see DynamoDB best practices for modeling data, choosing keys, avoiding hot partitions and controlling the bill. The guide is for engineers, architects and DBAs who run DynamoDB in production, or who are about to. It covers data modeling and single-table design, partition and sort keys, GSIs, transactions and replication, cost, and operations, followed by a common anti-patterns table and a production readiness checklist.</p>
<p>Amazon DynamoDB is a managed key-value and document database. It has no versions and no maintenance windows, so new features ship continuously, and the notes in this guide reflect AWS announcements up to October 2026. The approach is access-pattern-first: you list how the application reads and writes data, then shape the keys and indexes around that list. Single-table design is explained as a tool for the right cases, not a universal rule.</p>
<p>Before we start, please check the prerequisites: basic familiarity with tables, items, partition key and sort key, plus an AWS account and a current SDK (boto3 for Python or the AWS SDK for .NET).</p>
<blockquote><p><strong>Version note:</strong> This guide targets the current DynamoDB service as of October 2026, including vector search (generally available Aug 5, 2026), global tables with multi-Region strong consistency (15 AWS Regions as of the Sept 2026 expansion) and the Streams AT_TIMESTAMP shard iterator (announced Oct 15, 2026).</p></blockquote>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>In this guide</strong></p>
<ul>
<li><a href="#dynamodb-best-practices-at-a-glance">DynamoDB Best Practices at a Glance</a></li>
<li><a href="#dynamodb-data-modeling-access-patterns-and-single-table-desi">DynamoDB Data Modeling: Access Patterns and Single-Table Design</a></li>
<li><a href="#partition-key-sort-key-and-gsi-design-avoid-hot-partitions">Partition Key, Sort Key and GSI Design: Avoid Hot Partitions</a></li>
<li><a href="#transactions-consistency-replication-and-streams">Transactions, Consistency, Replication and Streams</a></li>
<li><a href="#dynamodb-cost-optimization-on-demand-vs-provisioned-and-item">DynamoDB Cost Optimization: On-Demand vs Provisioned and Item Size</a></li>
<li><a href="#dynamodb-security-backups-monitoring-anti-patterns-and-produ">DynamoDB Security, Backups, Monitoring, Anti-Patterns and Production Checklist</a></li>
<li><a href="#key-takeaways">Key takeaways</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2></h2>
<h2 id="dynamodb-best-practices-at-a-glance">DynamoDB Best Practices at a Glance</h2>
<p>Today in this guide, we will see how to design, run and pay for Amazon DynamoDB in production. DynamoDB is a fully managed NoSQL key-value and document database from AWS. We will cover data modeling, key design, indexes, transactions, replication, cost, security, backups and monitoring.</p>
<p><strong>Version note:</strong> DynamoDB has no versions or maintenance windows, because features ship continuously. This guide is current to the Oct 2026 announcements.</p>
<blockquote><p><strong>In plain English:</strong> A partition is a physical storage unit that serves a slice of your table&#8217;s keys. Think of one checkout counter among many in a supermarket: each counter handles only some customers, and one counter can only serve so many people at a time.</p></blockquote>
<p>The table below shows the main rules for each area. Later sections explain the reasoning behind each one.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Area</th>
<th>Do this</th>
<th>Avoid this</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Data modeling</strong></td>
<td>Start from access patterns, then shape the table (DynamoDB single table design where patterns are known and related)</td>
<td>Copying a relational schema and expecting joins</td>
</tr>
<tr>
<td><strong>Partition key</strong></td>
<td>Use a high-cardinality key with even traffic (e.g. orderId)</td>
<td>Timestamp or status as the partition key</td>
</tr>
<tr>
<td><strong>Sort key</strong></td>
<td>Use it for ordering and range queries inside one item collection</td>
<td>Putting unrelated data under one sort key scheme</td>
</tr>
<tr>
<td><strong>GSIs</strong></td>
<td>Add one per proven access pattern, keep it sparse, project only needed attributes</td>
<td>One GSI per query idea</td>
</tr>
<tr>
<td><strong>Query and reads</strong></td>
<td>Use Query, ProjectionExpression and eventually consistent reads where staleness is fine</td>
<td>Scan in hot paths, FilterExpression as an index</td>
</tr>
<tr>
<td><strong>Transactions</strong></td>
<td>Use conditional writes first, transactions only for all-or-nothing needs</td>
<td>Wrapping every write in a transaction</td>
</tr>
<tr>
<td><strong>Replication</strong></td>
<td>Pick global table mode by the consistency you need</td>
<td>Assuming cross-Region reads are always fresh</td>
</tr>
<tr>
<td><strong>Capacity mode</strong></td>
<td>Start with on-demand; move to provisioned only for steady traffic</td>
<td>Leaving over-provisioned capacity unchanged</td>
</tr>
<tr>
<td><strong>Item size and blobs</strong></td>
<td>Keep blobs in S3 and store a reference</td>
<td>Large payloads inline in items</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td>Least-privilege IAM, encryption, private endpoints</td>
<td>Broad table-wide permissions, secrets in attributes</td>
</tr>
<tr>
<td><strong>Backups</strong></td>
<td>Turn on PITR and deletion protection</td>
<td>Never testing a restore</td>
</tr>
<tr>
<td><strong>Monitoring</strong></td>
<td>Alarm on throttles, use Contributor Insights</td>
<td>Finding hot keys only after users complain</td>
</tr>
</tbody>
</table>
</figure>
<h2 id="dynamodb-data-modeling-access-patterns-and-single-table-desi">DynamoDB Data Modeling: Access Patterns and Single-Table Design</h2>
<p>For <a href="https://thecodebuzz.com/what-is-data-modelling-fundamentals-sql-nosql-guidelines/">DynamoDB data modeling</a>, <strong>list your access patterns first</strong> (e.g. &#8220;get customer by ID&#8221;, &#8220;list a customer&#8217;s orders newest first&#8221;) and shape the table to serve them. DynamoDB has no joins, so the table layout has to answer each question with a single key lookup or Query.</p>
<h3>What Is Single-Table Design?</h3>
<p>DynamoDB single table design means you <a href="https://thecodebuzz.com/mongodb-schema-design-guidelines-best-practices/">denormalize and pre-join data</a> at write time. Related items share a partition key, so one Query returns them together. The table shape follows how the data is read, not how it is normalized.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note :</strong> A single-table layout is like filing a customer&#8217;s invoice, address and receipts in one folder. You pull one folder instead of visiting three cabinets. The analogy stops fitting when the folder grows very large, because one key has throughput limits (covered in the next section).</p></blockquote>
<h3>Which Key Conventions Should You Follow?</h3>
<p>Use generic <code>PK</code> and <code>SK</code> attribute names, prefix values with the entity type (e.g. <code>USER#</code>, <code>ORDER#</code>), and add an <code>entityType</code> attribute. The attribute helps when debugging and lets stream consumers route each item to the right handler.</p>
<p>The table below shows a small item collection for a retail customer and orders. One Query on <code>PK = USER#1001</code> returns the profile and all orders. Adding <code>begins_with(SK, "ORDER#")</code> returns only the orders.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>PK</th>
<th>SK</th>
<th>entityType</th>
<th>Attributes</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>USER#1001</strong></td>
<td>PROFILE</td>
<td>User</td>
<td>name, email</td>
</tr>
<tr>
<td><strong>USER#1001</strong></td>
<td>ORDER#2026-10-20#A17</td>
<td>Order</td>
<td>total, status</td>
</tr>
<tr>
<td><strong>USER#1001</strong></td>
<td>ORDER#2026-10-25#A18</td>
<td>Order</td>
<td>total, status</td>
</tr>
</tbody>
</table>
</figure>
<h3>How Do You Model Relationships?</h3>
<p>Model one-to-many with a shared PK and SK prefixes, as in the table above. Model many-to-many with the <strong>adjacency list pattern</strong>, where each relationship is its own item.</p>
<ul>
<li><strong>Education example:</strong> store <code>PK = STUDENT#7</code>, <code>SK = COURSE#MATH101</code> and the mirror item <code>PK = COURSE#MATH101</code>, <code>SK = STUDENT#7</code>. A Query on either side lists the other.</li>
<li><strong>Retail example:</strong> do the same for products and categories. A GSI that swaps the keys can also serve the reverse lookup.</li>
</ul>
<h3>When Should You Not Use a Single Table?</h3>
<p>Avoid a single table when entities scale or fail independently, need separate IAM, backup or blast radius, or have simple or still-changing access patterns. It pays off when patterns are <strong>known, related and high volume</strong>. The animated diagram below walks through this decision step by step.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/dynamodb-best-practices-single-table-design-diagram-2.gif" alt="Decision tree: known stable patterns, co-read high-volume entities and no isolation needs lead to a single table; any other answer leads to separate tables." width="1200" height="675" /><figcaption>Choose a single table only when access patterns are known, related and high volume, and the entities don&#8217;t need isolation.</figcaption></figure>
<p>The table compares the two approaches across the main concerns.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Concern</th>
<th>Single table</th>
<th>Separate tables</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Scaling</strong></td>
<td>Shared table settings</td>
<td>Tuned per entity</td>
</tr>
<tr>
<td><strong>IAM and backup scope</strong></td>
<td>One policy and one backup for everything</td>
<td>Separate policies, backups and blast radius</td>
</tr>
<tr>
<td><strong>Change tolerance</strong></td>
<td>New patterns may need remodeling or backfills</td>
<td>Easier to adapt</td>
</tr>
<tr>
<td><strong>Readability</strong></td>
<td>Harder; needs documented key conventions</td>
<td>Familiar, one entity per table</td>
</tr>
</tbody>
</table>
</figure>
<h3>How Do Vector Search and Large Blobs Affect Modeling?</h3>
<p>Native vector search became generally available on Aug 5, 2026. Embeddings sit next to your other attributes, and a vector index type works alongside GSIs and LSIs. For many RAG and semantic-search cases, this removes the need for a second database. AWS claims single-digit millisecond latency at 99%+ recall, so verify that against your own data.</p>
<p>Keep large blobs (images, PDFs, long documents) in S3 and store only the reference in the item. Item size drives read and write units, so a lean item is cheaper to read and write. The cost section explains the arithmetic.</p>
<h2 id="partition-key-sort-key-and-gsi-design-avoid-hot-partitions">Partition Key, Sort Key and GSI Design: Avoid Hot Partitions</h2>
<p>Choose a <strong>high-cardinality partition key</strong> (many distinct values) whose traffic spreads evenly, and use the sort key for ordering and range queries inside one item collection. AWS advises designing for uniform activity across all partition keys in the table and in its secondary indexes.</p>
<h3>What are the per-partition limits in DynamoDB?</h3>
<p>Each partition is designed to deliver up to <strong>3,000 read units and 1,000 write units per second</strong>. Item size counts toward these limits.</p>
<ul>
<li><strong>Read unit:</strong> one strongly consistent read per second, or two eventually consistent reads per second, for an item up to 4 KB.</li>
<li><strong>Write unit:</strong> one write per second for an item up to 1 KB.</li>
</ul>
<p>Example: a 20 KB item costs 5 read units per strongly consistent read (20 / 4). About 600 reads per second on that single item uses 3,000 read units, which is the partition limit.</p>
<h3>What does adaptive capacity fix, and what does it not?</h3>
<p>Adaptive capacity works in both on-demand and provisioned modes and helps when traffic is uneven across keys. It cannot raise the hard per-partition limit for one hot key. Total throughput is still bounded by your provisioned throughput or the on-demand table limit.</p>
<blockquote><p><strong>In plain English:</strong> A hot partition is one checkout lane with a huge queue while the other lanes sit empty. Adding more lanes does not help if every customer insists on the same lane.</p></blockquote>
<h3>Which partition keys work and which fail?</h3>
<p>The table below compares common choices for DynamoDB partition key design.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Key</th>
<th>Why it works or fails</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>userId, orderId</strong></td>
<td>Many distinct values, traffic spreads well.</td>
<td>Use as is.</td>
</tr>
<tr>
<td><strong>date, timestamp</strong></td>
<td>All current writes hit today&#8217;s key.</td>
<td>Move time to the sort key.</td>
</tr>
<tr>
<td><strong>status, boolean</strong></td>
<td>Only a few values, so a few partitions take everything.</td>
<td>Use a sparse GSI instead.</td>
</tr>
<tr>
<td><strong>country</strong></td>
<td>Low cardinality, skewed by population.</td>
<td>Combine with another attribute.</td>
</tr>
<tr>
<td><strong>tenantId</strong></td>
<td>Risky when one tenant is far larger than the rest.</td>
<td>Write-shard the large tenant.</td>
</tr>
</tbody>
</table>
</figure>
<h3>How do you design the sort key?</h3>
<p>The sort key orders items inside one partition key and enables range queries. Use ISO 8601 timestamps or prefixed values such as <code>ORDER#2026-10-25</code>, because they sort correctly as strings. Sort keys compare as plain strings, so a longer value sorts after its shorter prefix.</p>
<ul>
<li><strong>begins_with:</strong> fetch all items for one prefix, e.g. <code>ORDER#2026-10</code> for a month.</li>
<li><strong>between:</strong> fetch a date range. Both bounds are inclusive, so choose the upper bound with care (see the pitfall below).</li>
<li><strong>Composite sort keys:</strong> encode hierarchies such as <code>COUNTRY#IN#CITY#PUNE</code> to query at any level.</li>
</ul>
<p>Here is the pitfall. Suppose your sort keys look like <code>ORDER#2026-10-25#A18</code> (date, then order ID). The condition <code>BETWEEN 'ORDER#2026-10-01' AND 'ORDER#2026-10-25'</code> silently drops every order placed on the 25th, because <code>ORDER#2026-10-25#A18</code> is longer than the upper bound and sorts after it. The lower bound is safe, since a shorter prefix sorts before the longer keys that start with it.</p>
<p>There are two simple fixes. Use the next day as the upper bound (<code>ORDER#2026-10-26</code>), or add a high character such as <code>~</code> to the end of the day (<code>ORDER#2026-10-25~</code>). The <code>~</code> character sorts after <code>#</code>, digits and letters in byte order, so it works when order IDs are plain alphanumeric text.</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">import boto3
from boto3.dynamodb.conditions import Key

table = boto3.resource("dynamodb").Table("Orders")

resp = table.query(
    KeyConditionExpression=Key("PK").eq("USER#u-1001")
    &amp; Key("SK").between("ORDER#2026-10-01", "ORDER#2026-10-25~")
)
orders = resp["Items"]</code></pre>
<p>This returns all orders from 1 October to 25 October 2026 for one customer, including those placed on the 25th.</p>
<h3>How does write sharding spread a hot key?</h3>
<p>Write sharding appends a calculated or random suffix (e.g. a hash of orderId modulo N) to one logical key. The trade-off is that reads become scatter-gather: you query all N shards and merge. The animated diagram below shows the write fan-out and the read path.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/dynamodb-best-practices-single-table-design-diagram-3.gif" alt="Hot tenant-42 writes throttle one partition, then a hash suffix spreads them over four shards; reads query all four and merge." width="1200" height="675" /><figcaption>Write sharding spreads one hot key over N suffixed keys, and reads pay for it by querying all N shards and merging.</figcaption></figure>
<p>The simplified example below writes with a stable hash suffix and reads the newest orders from four shards. Python&#8217;s built-in <code>hash()</code> changes between processes, so use <code>hashlib</code>.</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">import hashlib
import boto3
from boto3.dynamodb.conditions import Key

SHARDS = 4
table = boto3.resource("dynamodb").Table("Orders")

def shard_for(order_id: str) -&gt; int:
    return int(hashlib.md5(order_id.encode()).hexdigest(), 16) % SHARDS

def put_order(tenant, order_id, created_at, attrs):
    table.put_item(Item={
        "PK": f"TENANT#{tenant}#{shard_for(order_id)}",
        "SK": f"ORDER#{created_at}#{order_id}",
        **attrs,
    })

def list_orders(tenant, limit=50):
    """Return the newest `limit` orders across all shards (first page only)."""
    items = []
    for shard in range(SHARDS):
        kwargs = {
            "KeyConditionExpression": Key("PK").eq(f"TENANT#{tenant}#{shard}"),
            "ScanIndexForward": False,
            "Limit": limit,
        }
        shard_items = []
        while len(shard_items) &lt; limit:
            resp = table.query(**kwargs)
            shard_items.extend(resp["Items"])
            if "LastEvaluatedKey" not in resp:
                break
            kwargs["ExclusiveStartKey"] = resp["LastEvaluatedKey"]
            kwargs["Limit"] = limit - len(shard_items)
        items.extend(shard_items)
    items.sort(key=lambda i: i["SK"], reverse=True)
    return items[:limit]</code></pre>
<p>Each shard is read only until it has <code>limit</code> items, so the function reads at most <code>SHARDS × limit</code> items. That is enough, because the newest <code>limit</code> orders overall must be among the newest <code>limit</code> of some shard. The <code>LastEvaluatedKey</code> loop is still needed because one query response is capped at 1 MB of data.</p>
<p>For a &#8220;next page&#8221; feature, keep one <code>LastEvaluatedKey</code> per shard and resume each shard from its own cursor. To fetch every order for a tenant instead, remove the <code>limit</code> check and expect to read all items on all shards. In production, run the shard queries in parallel.</p>
<h3>What are the GSI best practices?</h3>
<p>Add a <a href="https://thecodebuzz.com/mongodb-indexes-performance-guidelines-and-best-practices/">global secondary index (GSI)</a> only for a proven access pattern. Each GSI repeats every matching write, so it adds write cost and storage. Apply the same uniform-key rule to GSI keys, because a skewed GSI key can throttle writes to the base table.</p>
<ul>
<li><strong>Sparse GSIs:</strong> only items that carry the GSI key attributes are copied. If only open orders have the key, the index stays small and cheap.</li>
<li><strong>Index overloading:</strong> use generic <code>GSI1PK</code> and <code>GSI1SK</code> so one index serves several entity types.</li>
<li><strong>GSI write sharding:</strong> shard the index key the same way as the table key.</li>
<li><strong>Narrow projections:</strong> use <code>KEYS_ONLY</code> or <code>INCLUDE</code> instead of <code>ALL</code> unless every attribute is needed.</li>
</ul>
<p>How many GSIs should a table have? As few as your proven access patterns need. The default quota has been 20 GSIs per table, but quotas can change and can be raised on request, so confirm the current number on the DynamoDB quotas page for your Region and account.</p>
<h3>What should you know about LSIs and the 10 GB item-collection limit?</h3>
<p>A local secondary index (LSI) uses the same partition key as the base table with a different sort key. It must be defined when the table is created, and the default quota has been 5 LSIs per table (confirm on the quotas page).</p>
<p>If a table has an LSI, the <strong>10 GB limit applies to each item collection</strong>, meaning all items (and their LSI entries) that share one partition key value. Writes to a collection that reaches the limit are rejected. Tables without an LSI don&#8217;t have this cap.</p>
<p>This is why an unbounded collection, such as every order for one tenant under a single partition key, is risky on a table with an LSI. Prefer a GSI when you need an alternate sort order, or bound collections with a time bucket or a shard suffix.</p>
<h3>How do you keep queries cheap?</h3>
<ul>
<li><strong>Use Query, not Scan:</strong> Query reads one item collection, while Scan reads the whole table.</li>
<li><strong>Add ProjectionExpression:</strong> return only the attributes you need.</li>
<li><strong>Use eventually consistent reads:</strong> they cost half as much when slightly stale data is acceptable.</li>
<li><strong>Don&#8217;t treat FilterExpression as an index:</strong> it filters after reading, so you still pay for all the data read.</li>
</ul>
<h2 id="transactions-consistency-replication-and-streams">Transactions, Consistency, Replication and Streams</h2>
<p>Use <strong>conditional writes</strong> for optimistic concurrency, use <code>TransactWriteItems</code> only where several items must succeed or fail together, and choose the replication model by the consistency you need.</p>
<h3>How do conditional writes prevent overwrites?</h3>
<p>Add a <code>ConditionExpression</code> to a write, and DynamoDB applies it only if the condition is true. Use <code>attribute_not_exists(PK)</code> to block overwriting an existing item. For optimistic locking, keep a <code>version</code> attribute and write with <code>version = :expected</code>, then increment it.</p>
<blockquote><p><strong>In plain English:</strong> A conditional write is like a ticket office checking the seat is still free at the moment you pay. If someone else took it first, your purchase is refused instead of silently replacing theirs.</p></blockquote>
<h3>When should you use TransactWriteItems?</h3>
<p><code>TransactWriteItems</code> is all-or-nothing across items. Keep it for cases such as moving an order status and writing a ledger item in finance. A transactional read costs 2 RRUs on-demand, against 1 for a strongly consistent read and 0.5 for an eventually consistent one.</p>
<p class="code-label"><strong>C# (.NET)</strong></p>
<pre><code class="language-csharp">using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;

// Create one client per process and reuse it.
var client = new AmazonDynamoDBClient();

var orderId = Guid.NewGuid().ToString();

var request = new TransactWriteItemsRequest
{
    ClientRequestToken = orderId, // same token on retry = same request
    TransactItems = new List&lt;TransactWriteItem&gt;
    {
        new TransactWriteItem
        {
            Put = new Put
            {
                TableName = "AppTable",
                Item = new Dictionary&lt;string, AttributeValue&gt;
                {
                    ["PK"] = new AttributeValue { S = "USER#42" },
                    ["SK"] = new AttributeValue { S = $"ORDER#{orderId}" },
                    ["entityType"] = new AttributeValue { S = "Order" }
                },
                ConditionExpression = "attribute_not_exists(PK)"
            }
        },
        new TransactWriteItem
        {
            Update = new Update
            {
                TableName = "AppTable",
                Key = new Dictionary&lt;string, AttributeValue&gt;
                {
                    ["PK"] = new AttributeValue { S = "USER#42" },
                    ["SK"] = new AttributeValue { S = "PROFILE" }
                },
                UpdateExpression = "ADD orderCount :one",
                ConditionExpression = "attribute_exists(PK)",
                ExpressionAttributeValues = new Dictionary&lt;string, AttributeValue&gt;
                {
                    [":one"] = new AttributeValue { N = "1" }
                }
            }
        }
    }
};

try
{
    await client.TransactWriteItemsAsync(request);
}
catch (TransactionCanceledException ex)
{
    foreach (var reason in ex.CancellationReasons)
        Console.WriteLine($"{reason.Code}: {reason.Message}");
}</code></pre>
<p>The order item and the customer counter commit together, or neither does. <code>CancellationReasons</code> tells you which condition failed.</p>
<h3>How should you handle retries and pagination?</h3>
<ul>
<li><strong>Trust SDK retries:</strong> The SDKs retry <a href="https://thecodebuzz.com/transient-errors-in-cloud-guidelines-and-resolution/">throttled requests with backoff</a> by default, so do not add custom tight retry loops.</li>
<li><strong>Reuse the client:</strong> Keep one client instance per process.</li>
<li><strong>Paginate:</strong> Loop on <code>LastEvaluatedKey</code> for every Query and Scan.</li>
</ul>
<h3>Which replication model fits your consistency needs?</h3>
<p>Global tables replicate across Regions. Multi-account global tables (announced Feb 3, 2026) add account-level isolation and separate governance. Multi-Region strong consistency (MRSC) is available in 15 AWS Regions as of the Sept 2026 expansion, so check your Regions first. In Oct 2026 AWS added replica disassociation, which lets a Region or account keep its table as a standalone single-Region table without copying data.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Model</th>
<th>Read freshness</th>
<th>Write latency</th>
<th>Use case</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Global table (eventual)</strong></td>
<td>Remote Regions may lag briefly</td>
<td>Local, lowest of the multi-Region options</td>
<td>Active-active apps, local reads</td>
</tr>
<tr>
<td><strong>Global table (MRSC)</strong></td>
<td>Strongly consistent across Regions</td>
<td>Higher, because Regions coordinate</td>
<td>Finance, no stale reads after failover</td>
</tr>
<tr>
<td><strong>Single Region</strong></td>
<td>Strong reads within the Region</td>
<td>Lowest</td>
<td>Simple apps, one-Region compliance</td>
</tr>
</tbody>
</table>
</figure>
<h3>What changed in DynamoDB Streams?</h3>
<p>Streams now supports the <code>AT_TIMESTAMP</code> shard iterator (announced Oct 15, 2026). A consumer can start from a point in time instead of only the oldest retained record (up to 24 hours old) or new records. It also works with KCL through the Streams Kinesis adapter.</p>
<p>In a single-table design, route stream consumers by the <code>entityType</code> attribute, so an order handler ignores customer records.</p>
<h2 id="dynamodb-cost-optimization-on-demand-vs-provisioned-and-item">DynamoDB Cost Optimization: On-Demand vs Provisioned and Item Size</h2>
<p>Start with <strong>on-demand</strong> for most workloads. Move to <a href="https://thecodebuzz.com/autoscaling-cloud-best-practices-azure-aws-google/">provisioned with auto scaling</a> only when traffic is steady and predictable. Cut cost first by shrinking items and removing unused indexes, because those savings apply in every capacity mode.</p>
<h3>Is on-demand or provisioned cheaper in DynamoDB?</h3>
<p>After the November 2024 on-demand price reduction, on-demand is commonly the default recommendation. Before that price cut, on-demand was widely seen as much more expensive for steady workloads. Provisioned capacity with auto scaling can still save money for steady traffic, but the gap has narrowed.</p>
<blockquote><p><strong>In plain English:</strong> On-demand is like paying per taxi ride, so you pay only when you travel. Provisioned is like leasing a car: cheaper per trip if you drive every day, wasted money if it sits in the garage.</p></blockquote>
<p>A secondary summary of the AWS cost-optimization page puts the break-even at about <strong>35% average utilization</strong> of provisioned capacity. That figure has not been confirmed against the AWS documentation, so treat it as unverified. It also says on-demand can still win above that level when traffic has quiet periods with occasional peaks. Check the current AWS cost-optimization page and your own CloudWatch usage data before you decide.</p>
<p>The animated diagram below shows the decision path for choosing a capacity mode.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/dynamodb-best-practices-single-table-design-diagram-5.gif" alt="Decision flow: new or unpredictable traffic uses on-demand; steady, high utilization uses provisioned with auto scaling; both can add a Savings Plan." width="1200" height="675" /><figcaption>Start on-demand, move to provisioned with auto scaling only for steady traffic, and re-check quarterly against CloudWatch.</figcaption></figure>
<h3>How do the capacity modes compare?</h3>
<p>This table compares the three options across the main decision factors.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Factor</th>
<th>On-demand</th>
<th>Provisioned</th>
<th>Provisioned + auto scaling</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Traffic shape</strong></td>
<td>New, spiky or unknown</td>
<td>Flat and known</td>
<td>Steady with gradual changes</td>
</tr>
<tr>
<td><strong>Throttling risk</strong></td>
<td>Low</td>
<td>High if under-provisioned</td>
<td>Medium; scaling reacts to load</td>
</tr>
<tr>
<td><strong>Planning effort</strong></td>
<td>None</td>
<td>High</td>
<td>Medium (targets and limits)</td>
</tr>
<tr>
<td><strong>Cost behaviour</strong></td>
<td>Pay per request</td>
<td>Pay for reserved units, used or not</td>
<td>Follows load, with some headroom</td>
</tr>
</tbody>
</table>
</figure>
<p><strong>AWS Database Savings Plans</strong> (launched December 2025) are a discount layer on top of a capacity mode, not a third mode. A 1-year commitment gives up to about 18% on on-demand throughput and up to about 12% on provisioned capacity.</p>
<h3>What does a read cost on-demand?</h3>
<p>Read cost depends on consistency and item size, in 4 KB steps. The table shows the on-demand read units (RRUs) per read of up to 4 KB.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Read type</th>
<th>Cost (up to 4 KB)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Strongly consistent</strong></td>
<td>1 RRU</td>
</tr>
<tr>
<td><strong>Eventually consistent</strong></td>
<td>0.5 RRU</td>
</tr>
<tr>
<td><strong>Transactional</strong></td>
<td>2 RRUs</td>
</tr>
</tbody>
</table>
</figure>
<p>For storage, the source quotes list prices of $0.25 per GB-month for Standard and $0.10 per GB-month for Standard-IA. Confirm prices for your Region on the AWS pricing page.</p>
<h3>How do I compare the modes with my own numbers?</h3>
<p>Convert your traffic into monthly units, then multiply by the list price for your Region. The formula is requests per second x units per request x seconds in the month x price per unit. This guide does not quote throughput prices, so take them from the AWS pricing page.</p>
<p>The example below uses a simplified workload: a retail catalog service that reads a 6 KB item with strongly consistent reads, averaging 100 reads per second with a peak of 200 reads per second. It assumes a 30-day month (2,592,000 seconds, 720 hours) and provisioned capacity set flat at the peak.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Step</th>
<th>On-demand</th>
<th>Provisioned (flat at peak)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Units per read</strong></td>
<td>6 KB rounds up to two 4 KB blocks = 2 RRUs</td>
<td>2 read units</td>
</tr>
<tr>
<td><strong>Rate used</strong></td>
<td>Average: 100 x 2 = 200 RRUs per second</td>
<td>Peak: 200 x 2 = 400 read capacity units</td>
</tr>
<tr>
<td><strong>Monthly quantity</strong></td>
<td>200 x 2,592,000 = 518.4 million RRUs</td>
<td>400 x 720 = 288,000 capacity-unit hours</td>
</tr>
<tr>
<td><strong>Monthly cost</strong></td>
<td>518.4 x on-demand price per million RRUs</td>
<td>288,000 x provisioned price per capacity-unit hour</td>
</tr>
<tr>
<td><strong>Average utilization</strong></td>
<td>Not applicable (pay per request)</td>
<td>200 / 400 = 50%</td>
</tr>
</tbody>
</table>
</figure>
<p>Plug both prices into the last row and compare the two totals. Repeat the same steps for writes, using 1 write unit per 1 KB. If you switch to eventually consistent reads, halve the read units in both columns.</p>
<p>This simplified Python script does the same arithmetic. It asks for the two prices so you can enter the current values for your Region, and you can add your own logic for writes or auto scaling.</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">import math

SECONDS_PER_MONTH = 30 * 24 * 3600
HOURS_PER_MONTH = 30 * 24


def read_units(item_kb, strongly_consistent=True):
    """Read units for one read: 4 KB blocks, halved for eventual consistency."""
    blocks = math.ceil(item_kb / 4)
    return blocks * (1 if strongly_consistent else 0.5)


def compare_read_cost(avg_rps, peak_rps, item_kb, od_price_per_million, prov_price_per_unit_hour):
    units = read_units(item_kb)
    on_demand_units = avg_rps * units * SECONDS_PER_MONTH
    provisioned_unit_hours = peak_rps * units * HOURS_PER_MONTH
    return {
        "on_demand_cost": on_demand_units / 1_000_000 * od_price_per_million,
        "provisioned_cost": provisioned_unit_hours * prov_price_per_unit_hour,
        "avg_utilization": avg_rps / peak_rps,
    }


od_price = float(input("On-demand price per million read units: "))
prov_price = float(input("Provisioned price per read capacity unit-hour: "))
print(compare_read_cost(100, 200, 6, od_price, prov_price))
</code></pre>
<p>The script returns both monthly costs and the average utilization, so you can see which mode is cheaper for your traffic shape.</p>
<h3>How do item size and GSIs multiply cost?</h3>
<p>A write unit covers 1 KB, so a 6 KB item write costs 6 write units. A strongly consistent read of the same item costs 2 read units (6 KB rounds up to two 4 KB blocks). Every GSI that contains the item repeats the write.</p>
<p>The table shows write units for one 6 KB item under different designs. KEYS_ONLY figures assume the projected keys fit in 1 KB.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Design</th>
<th>Write units per 6 KB item</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>No GSI</strong></td>
<td>6</td>
</tr>
<tr>
<td><strong>3 GSIs, ALL projection, item in every index</strong></td>
<td>Up to 24 (6 x 4)</td>
</tr>
<tr>
<td><strong>3 GSIs, ALL projection, item in only one (sparse)</strong></td>
<td>12</td>
</tr>
<tr>
<td><strong>3 GSIs, KEYS_ONLY projection</strong></td>
<td>About 9 (6 + 3 x 1)</td>
</tr>
</tbody>
</table>
</figure>
<h3>Which other levers reduce the DynamoDB bill?</h3>
<ul>
<li><strong>Standard-IA table class:</strong> Use it for data that is stored a lot but read rarely (e.g. old retail orders).</li>
<li><strong>TTL:</strong> Expire old items such as sessions so you stop paying to store them.</li>
<li><strong>Blobs in S3:</strong> Keep only a reference in the item to shrink read and write units.</li>
<li><strong>Short attribute names:</strong> Apply only where item size is truly the cost driver, since they hurt readability.</li>
<li><strong>Eventually consistent reads:</strong> Use them where slightly stale data is acceptable, to halve read cost.</li>
<li><strong>PITR and backups:</strong> Track their storage as its own line item on the bill.</li>
</ul>
<h2 id="dynamodb-security-backups-monitoring-anti-patterns-and-produ">DynamoDB Security, Backups, Monitoring, Anti-Patterns and Production Checklist</h2>
<p>Turn on <strong>point-in-time recovery (PITR)</strong> and deletion protection, give each application least-privilege IAM access, and alarm on throttles before users notice them. DynamoDB has no servers to patch, so most of your production work is access control, recovery and watching the right metrics.</p>
<h3>How do you secure a DynamoDB table?</h3>
<p>Grant only the actions and keys each application needs, and keep traffic private. The points below cover the main controls:</p>
<ul>
<li><strong>Least-privilege IAM:</strong> Scope policies to one table, and to specific actions such as <code>dynamodb:GetItem</code> and <code>dynamodb:Query</code>. Avoid <code>dynamodb:*</code> on all tables.</li>
<li><strong>Key-prefix access:</strong> Use the <code>dynamodb:LeadingKeys</code> condition key to limit a caller to partition keys it owns (e.g. one tenant&#8217;s <code>TENANT#</code> prefix in a SaaS product).</li>
<li><strong>Encryption at rest:</strong> It is on by default. You do not need to enable it, but check which key type your compliance rules require.</li>
<li><strong>Private access:</strong> Use a VPC gateway endpoint so traffic from your VPC does not travel over the public internet.</li>
<li><strong>Account isolation:</strong> Keep production in its own account. Multi-account global tables help when replicas need separate governance.</li>
<li><strong>Sensitive data:</strong> Do not store secrets or unencrypted personal data (e.g. patient identifiers in a healthcare app) in plain attributes. Keep secrets in a secrets manager.</li>
</ul>
<h3>How do you back up and restore DynamoDB?</h3>
<p>Use PITR for continuous recovery inside its retention window, and on-demand backups for long-term copies. Both add storage cost, so treat them as a budget line item.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note :</strong> PITR is like a security camera that records continuously. If someone deletes the wrong rows at 2:10 PM, you rewind to 2:09 PM. It only keeps footage for a limited time, so older copies need on-demand backups.</p></blockquote>
<p>The retention window is configurable. You set the recovery period in the same PITR specification (the <code>RecoveryPeriodInDays</code> field), so pick a window that matches how long it could take you to notice a bad deployment or a bad delete. Check the current AWS documentation for the allowed range, and remember that a longer window means more backup storage.</p>
<p>The commands below enable PITR and deletion protection on a table named <code>Orders</code>:</p>
<p class="code-label"><strong>AWS CLI</strong></p>
<pre><code class="language-bash">aws dynamodb update-continuous-backups \
  --table-name Orders \
  --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true

aws dynamodb update-table \
  --table-name Orders \
  --deletion-protection-enabled</code></pre>
<p>The first command turns on continuous backups with the default retention window. The second blocks the table from being deleted until you switch protection off.</p>
<h3>What does a DynamoDB restore bring back, and what do you reapply?</h3>
<p>A restore always creates a new table. By default it brings back the data and the table&#8217;s GSIs and LSIs, but not auto scaling, IAM, alarms or TTL, so you must set those up again on the new table.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Item</th>
<th>Restored?</th>
<th>What you do</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Table data</strong></td>
<td>Yes</td>
<td>Verify with spot-check queries</td>
</tr>
<tr>
<td><strong>GSIs and LSIs</strong></td>
<td>Yes, by default</td>
<td>Confirm they are present and active before sending traffic</td>
</tr>
<tr>
<td><strong>Auto scaling settings</strong></td>
<td>No</td>
<td>Reapply the scaling policies if you use provisioned mode</td>
</tr>
<tr>
<td><strong>IAM policies</strong></td>
<td>No</td>
<td>Update policies to the new table name and ARN</td>
</tr>
<tr>
<td><strong>CloudWatch alarms</strong></td>
<td>No</td>
<td>Recreate the throttle and error alarms</td>
</tr>
<tr>
<td><strong>TTL setting</strong></td>
<td>No</td>
<td>Enable TTL again on the expiry attribute</td>
</tr>
</tbody>
</table>
</figure>
<p>Keep these settings in infrastructure-as-code so reapplying them is a repeatable step, not a manual checklist. Also check the AWS restore documentation for other table settings (e.g. stream settings and tags) that you may need to review. Rehearse the restore on a schedule so the first time you do it is not during an incident.</p>
<h3>What changes for operations and upgrades?</h3>
<p>Nothing needs patching. There are no OS settings, memory tuning, maintenance windows or version upgrades, because features ship continuously. Instead, review new announcements during architecture reviews (e.g. Streams <code>AT_TIMESTAMP</code>, vector search, replica disassociation).</p>
<p>Use TTL to expire old items. Expiry is not instant, so filter expired items in reads if stale data would cause problems.</p>
<h3>Which metrics should you monitor?</h3>
<p>Alarm on throttling and errors first, then watch consumption. Track <code>ThrottledRequests</code>, <code>ReadThrottleEvents</code>, <code>WriteThrottleEvents</code>, <code>SystemErrors</code>, <code>ConsumedReadCapacityUnits</code>, <code>ConsumedWriteCapacityUnits</code> and, for global tables, <code>ReplicationLatency</code>.</p>
<p>Enable CloudWatch Contributor Insights to see the most accessed and most throttled keys. It is the fastest way to confirm a DynamoDB hot partition.</p>
<h3>What are the common DynamoDB anti-patterns?</h3>
<p>This table maps each mistake to what you will see and how to fix it:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Anti-pattern</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Timestamp or status as partition key</strong></td>
<td>Throttling on a few keys</td>
<td>Use a high-cardinality key such as <code>orderId</code></td>
</tr>
<tr>
<td><strong>Scan in the request path</strong></td>
<td>Slow reads, high read cost</td>
<td>Use Query with a purpose-built key or GSI</td>
</tr>
<tr>
<td><strong>One GSI per idea</strong></td>
<td>High write cost and storage</td>
<td>Keep only proven indexes; use sparse and overloaded GSIs</td>
</tr>
<tr>
<td><strong>Blobs stored inline</strong></td>
<td>Large items, expensive reads and writes</td>
<td>Store in S3, keep a reference</td>
</tr>
<tr>
<td><strong>Unbounded item collection with huge items</strong></td>
<td>One key grows and gets hot</td>
<td>Split the key or shard it</td>
</tr>
<tr>
<td><strong>FilterExpression as an index</strong></td>
<td>Paying to read data you discard</td>
<td>Model the filter into the key or a GSI</td>
</tr>
<tr>
<td><strong>Single table on a simple, changing domain</strong></td>
<td>Hard-to-read model, painful changes</td>
<td>Use separate tables</td>
</tr>
<tr>
<td><strong>Over-provisioned capacity left unchanged</strong></td>
<td>Low utilization, high bill</td>
<td>Review the capacity mode and auto scaling</td>
</tr>
<tr>
<td><strong>No PITR or deletion protection</strong></td>
<td>Accidental loss is permanent</td>
<td>Enable both</td>
</tr>
<tr>
<td><strong>Hot tenant, no sharding</strong></td>
<td>One large tenant is throttled</td>
<td>Add a write-shard suffix to the key</td>
</tr>
</tbody>
</table>
</figure>
<h3>Production readiness checklist</h3>
<p>Work through these steps in order before go-live:</p>
<ol>
<li><strong>List access patterns:</strong> Test each one with a real Query.</li>
<li><strong>Check key cardinality:</strong> Confirm traffic spreads evenly across partition keys.</li>
<li><strong>Justify every GSI:</strong> Document each index and its projection.</li>
<li><strong>Set an item size and blob policy:</strong> Decide what goes to S3.</li>
<li><strong>Choose the capacity mode:</strong> Document why.</li>
<li><strong>Enable PITR and deletion protection:</strong> Set the PITR retention window deliberately, and add TTL where data expires.</li>
<li><strong>Apply least-privilege IAM:</strong> Use <code>dynamodb:LeadingKeys</code> where callers share a table.</li>
<li><strong>Configure alarms:</strong> Alert on throttles, and enable Contributor Insights.</li>
<li><strong>Run a restore test:</strong> Check that the data and GSIs/LSIs are present, then reapply auto scaling, IAM, TTL and alarms from your infrastructure-as-code.</li>
<li><strong>Load-test the hottest key:</strong> Compare results with the per-partition limits.</li>
</ol>
<h2 id="key-takeaways">Key takeaways</h2>
<p>These are the points to remember from the guide, in the order you will usually need them.</p>
<ul>
<li><strong>Design from access patterns first:</strong> list every read and write before you choose keys. Single-table design is a tool for known, related, high-volume patterns, not a universal rule. Use separate tables when entities scale or fail independently, or when patterns are simple or still changing.</li>
<li><strong>Use a high-cardinality partition key:</strong> one partition is designed for up to 3,000 read units and 1,000 write units per second, and item size counts toward that. Adaptive capacity helps with uneven traffic, but it cannot lift the cap for one hot key. Avoid timestamps and status values as partition keys, and use write sharding when one logical key is genuinely hot.</li>
<li><strong>Keep GSIs few, sparse and narrowly projected:</strong> every GSI repeats the write, so it adds write cost and storage. Add one only for a proven access pattern, and prefer KEYS_ONLY or INCLUDE over ALL. Apply the same even-spread rule to GSI keys, because a skewed index key can throttle base-table writes.</li>
<li><strong>Prefer Query over Scan:</strong> use <code>ProjectionExpression</code> and eventually consistent reads where staleness is acceptable. <code>FilterExpression</code> is not an index, because you still pay for everything it reads before filtering.</li>
<li><strong>Use the right tool for consistency:</strong> use conditional writes for optimistic concurrency, and <code>TransactWriteItems</code> only when several items must succeed or fail together. Transactional reads cost more than normal reads. Pick the global table model (eventual consistency, MRSC or single Region) by the freshness you actually need.</li>
<li><strong>Start on-demand, then measure:</strong> move to provisioned with auto scaling only when traffic is steady and predictable. Check the roughly 35% utilization break-even against the current AWS cost-optimization page and your own usage data, since it comes from a secondary summary. Database Savings Plans are a discount layer on top of a capacity mode, not a third mode.</li>
<li><strong>Treat item size as a cost driver:</strong> keep large blobs in S3 and store only the reference in the item. Use TTL to expire old data and Standard-IA for rarely read tables, and watch PITR and backup storage as a line item.</li>
<li><strong>Protect and observe the table:</strong> enable PITR and deletion protection, apply least-privilege IAM, and alarm on throttle metrics. Use Contributor Insights to confirm hot keys, and run restore tests on a schedule.</li>
<li><strong>Review new features when you revisit the design:</strong> DynamoDB has no versions, so check announcements such as multi-account global tables, MRSC, replica disassociation, Streams AT_TIMESTAMP and vector search. Each may simplify an architecture you built around older limits.</li>
</ul>
<h2 id="faq">FAQ</h2>
<h3><strong>Is single-table design always the best approach in DynamoDB?</strong></h3>
<p>No. Single-table design pays off when your access patterns are known, related and high volume, because one Query can return several entity types from the same partition key. Use separate tables when entities scale or fail independently, need separate IAM, backup scope or blast radius, or when the access patterns are simple or still changing.</p>
<h3><strong>How do I choose a good partition key in DynamoDB?</strong></h3>
<p>Choose an attribute with high cardinality (many distinct values) whose traffic is spread evenly, and that matches your main lookup, such as <code>customerId</code> or <code>orderId</code>. Avoid timestamps and status or boolean values, because they send most traffic to a few key values. Apply the same rule to the keys of your GSIs, since a skewed index key can throttle base-table writes.</p>
<h3><strong>What is a hot partition and how do I fix it?</strong></h3>
<p>A hot partition is a partition that receives more traffic than its limit, so requests get throttled even when the table as a whole is well under its capacity. Confirm it with CloudWatch Contributor Insights, which shows the most accessed and most throttled keys. Then fix it with a better partition key, write sharding (a calculated suffix that spreads one logical key across N keys), or caching for read-heavy keys. Adaptive capacity helps uneven traffic, but it cannot lift the hard limit for a single hot key.</p>
<h3><strong>What are the throughput limits per partition in DynamoDB?</strong></h3>
<p>A partition is designed to deliver up to 3,000 read units and 1,000 write units per second, and item size counts toward both limits. A read unit is one strongly consistent read (or two eventually consistent reads) per second for an item up to 4 KB. For example, a 20 KB item costs 5 read units per strongly consistent read, so about 600 reads per second on that one item reaches the partition limit.</p>
<h3><strong>Should I use on-demand or provisioned capacity?</strong></h3>
<p>Start with on-demand for most workloads, because it needs no capacity planning and has lower throttling risk for spiky traffic. Move to provisioned with auto scaling when traffic is steady and predictable. One secondary summary of AWS guidance puts the break-even at roughly 35% average utilization of provisioned capacity, so check that figure against the current AWS cost-optimization page and your own usage data. AWS Database Savings Plans are a discount on top of either mode, not a third mode.</p>
<p><strong>That&#8217;s all! Happy coding!</strong> Did this DynamoDB best practices guide help you solve your design or cost issue? Please share your questions or experience in the comments below. Bookmark this page and share it with your team, and subscribe to TheCodeBuzz for more database best-practice guides.</p>
<h2 id="sources">Sources and further reading</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html" target="_blank" rel="noopener">Best practices for designing and using partition keys effectively in DynamoDB</a> (AWS Documentation (Amazon DynamoDB Developer Guide))</li>
<li><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/dynamodb-data-modeling/best-practices.html" target="_blank" rel="noopener">Best practices &#8211; AWS Prescriptive Guidance (DynamoDB data modeling)</a> (AWS Prescriptive Guidance)</li>
<li><a href="https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-dynamodb-vector-search/" target="_blank" rel="noopener">Amazon DynamoDB now supports real-time vector search</a> (AWS What&#8217;s New)</li>
<li><a href="https://aws.amazon.com/about-aws/whats-new/2026/10/dynamodb-global-tables-disassociate/" target="_blank" rel="noopener">Amazon DynamoDB global tables now support disassociating replicas</a> (AWS What&#8217;s New)</li>
<li><a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-dynamodb-streams-timestamp-processing/" target="_blank" rel="noopener">Amazon DynamoDB Streams now supports timestamp-based stream processing</a> (AWS What&#8217;s New)</li>
</ul>
<p>&nbsp;</p><p>The post <a href="https://thecodebuzz.com/dynamodb-best-practices-single-table-design/">DynamoDB Best Practices: Single-Table Design, Keys and Cost</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/dynamodb-best-practices-single-table-design/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cassandra Best Practices: Data Modeling, Consistency, Operations</title>
		<link>https://thecodebuzz.com/cassandra-best-practices-guidelines/</link>
					<comments>https://thecodebuzz.com/cassandra-best-practices-guidelines/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Sun, 25 Oct 2026 02:51:21 +0000</pubDate>
				<category><![CDATA[Database Best Practices]]></category>
		<category><![CDATA[Cassandra]]></category>
		<category><![CDATA[Cassandra 5.0]]></category>
		<category><![CDATA[Data Modeling]]></category>
		<category><![CDATA[NoSQL]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30906</guid>

					<description><![CDATA[<p>Cassandra best practices for data modeling, consistency levels, repair, backups and upgrades, with rules and a production checklist for 5.0.</p>
<p>The post <a href="https://thecodebuzz.com/cassandra-best-practices-guidelines/">Cassandra Best Practices: Data Modeling, Consistency, Operations</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Today in this article, we will see the Cassandra best practices that matter for a production cluster. We cover data modeling, consistency and day-2 operations, so you can design the cluster and also keep it healthy after go-live.</p>
<p>The scope includes query-first modeling, partition key design, Storage-Attached Indexes (SAI), consistency levels, lightweight transactions (LWT) and Accord, tombstones, compaction, drivers, security, repair, backups, monitoring and upgrades. Every rule comes with the reason behind it, so you can judge when it fits your workload and when it does not.</p>
<p>Many popular guides still carry pre-5.0 advice. This guide adds what they often miss: the limits of SAI, UCS versus the STCS default, repair tied to <code>gc_grace_seconds</code>, driver settings, security and restore testing. It is written for engineers, architects and DBAs who run Cassandra in production.</p>
<p>For the version baseline, Apache Cassandra 5.0 is the latest GA series (5.0.9, released 2026-08-07), and 4.1 is the previous stable line (4.1.12). The 3.x series is end-of-life. Cassandra 6.0 is still pre-GA (6.0-alpha3) and is not for production.</p>
<blockquote><p><strong>Version note:</strong> This guide targets Apache Cassandra 5.0.9 (latest GA) and 4.1.12 (previous stable line), and treats 6.0-alpha3 as pre-GA only.</p>
<p>&nbsp;</p>
<p><strong>In this guide</strong></p>
<p>&nbsp;</p></blockquote>
<ul>
<li><a href="#cassandra-best-practices-at-a-glance">Cassandra Best Practices at a Glance</a></li>
<li><a href="#cassandra-data-modeling-best-practices-and-partition-key-des">Cassandra Data Modeling Best Practices and Partition Key Design</a></li>
<li><a href="#indexing-in-cassandra-5-0-sai-secondary-indexes-or-more-tabl">Indexing in Cassandra 5.0: SAI, Secondary Indexes or More Tables</a></li>
<li><a href="#consistency-levels-and-replication-factor-choosing-local-quo">Consistency Levels and Replication Factor: Choosing LOCAL_QUORUM</a></li>
<li><a href="#transactions-and-lightweight-transactions-paxos-v2-and-accor">Transactions and Lightweight Transactions: Paxos v2 and Accord in Cassandra 6.0</a></li>
<li><a href="#tombstones-ttl-and-gc-grace-seconds-deleting-data-safely">Tombstones, TTL and gc_grace_seconds: Deleting Data Safely</a></li>
<li><a href="#choosing-a-compaction-strategy-stcs-lcs-twcs-or-ucs">Choosing a Compaction Strategy: STCS, LCS, TWCS or UCS</a></li>
<li><a href="#drivers-connection-management-and-security">Drivers, Connection Management and Security</a></li>
<li><a href="#repair-backup-and-restore-best-practices">Repair, Backup and Restore Best Practices</a></li>
<li><a href="#operations-monitoring-and-upgrades">Operations, Monitoring and Upgrades</a></li>
<li><a href="#common-cassandra-anti-patterns">Common Cassandra Anti-Patterns</a></li>
<li><a href="#cassandra-production-readiness-checklist">Cassandra Production Readiness Checklist</a></li>
<li><a href="#key-takeaways">Key takeaways</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="cassandra-best-practices-at-a-glance">Cassandra Best Practices at a Glance</h2>
<p>Apache Cassandra is a distributed, wide-column database built for high write throughput and availability across nodes and datacenters. Today in this guide, we will see Cassandra data modeling best practices, consistency, indexing, repair, backups and upgrades in one place.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note:</strong> A partition is like one drawer in a filing cabinet. Every row with the same partition key goes into the same drawer, and that drawer lives on specific nodes. Unlike a real cabinet, each drawer is copied to several nodes, so one failure doesn&#8217;t lose it.</p></blockquote>
<h3>Best Practices Summary Table</h3>
<p>The table below lists the main areas, what to do and what to avoid. Each row is explained in its own section later.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Area</th>
<th>Do this</th>
<th>Avoid this</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Data modeling</strong></td>
<td>Model one table per query and denormalize.</td>
<td>Designing tables from entities and expecting joins.</td>
</tr>
<tr>
<td><strong>Partition key</strong></td>
<td>Use a high-cardinality key and keep partitions under about 100 MB and 100,000 items.</td>
<td>Low-cardinality keys (e.g. country, status) and unbounded partitions.</td>
</tr>
<tr>
<td><strong>Indexing</strong></td>
<td>Use extra tables for hot queries and SAI for secondary lookups.</td>
<td>Legacy secondary indexes in new designs.</td>
</tr>
<tr>
<td><strong>Consistency</strong></td>
<td>Use LOCAL_QUORUM for reads and writes in multi-datacenter clusters.</td>
<td>ALL, or cross-datacenter QUORUM by default.</td>
</tr>
<tr>
<td><strong>Replication factor</strong></td>
<td>Use RF=3 per datacenter with NetworkTopologyStrategy.</td>
<td>SimpleStrategy in multi-datacenter production.</td>
</tr>
<tr>
<td><strong>Transactions</strong></td>
<td>Use LWT only for compare-and-set on one partition.</td>
<td>LWT as the default write path.</td>
</tr>
<tr>
<td><strong>Deletes and TTL</strong></td>
<td>Prefer TTL and time bucketing over explicit deletes.</td>
<td>Queue-style tables and explicit null writes.</td>
</tr>
<tr>
<td><strong>Compaction</strong></td>
<td>Pick a strategy per table (UCS, or TWCS for TTL-heavy time series).</td>
<td>Leaving every table on the default without review.</td>
</tr>
<tr>
<td><strong>Drivers</strong></td>
<td>Use one long-lived session, prepared statements and an explicit local datacenter.</td>
<td>One session per request.</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td>Enable authentication, authorization and TLS, and replace the default superuser.</td>
<td>Running with defaults open.</td>
</tr>
<tr>
<td><strong>Repair</strong></td>
<td>Finish repair inside gc_grace_seconds (default 864000 seconds, 10 days).</td>
<td>Rare or skipped repair.</td>
</tr>
<tr>
<td><strong>Backups</strong></td>
<td>Schedule snapshots, store them off the node and test restores.</td>
<td>Snapshots that were never restored.</td>
</tr>
<tr>
<td><strong>Monitoring</strong></td>
<td>Watch partition size, pending compactions, latency and dropped messages.</td>
<td>Looking only at CPU and disk.</td>
</tr>
<tr>
<td><strong>Upgrades</strong></td>
<td>Move off 3.x because it is end-of-life, and roll one node at a time.</td>
<td>Staying on 3.x or running pre-GA builds.</td>
</tr>
</tbody>
</table>
</figure>
<h3>Which Cassandra Version Does This Guide Assume?</h3>
<p>This guide uses the versions below as its baseline.</p>
<ul>
<li><strong>5.0.9:</strong> the latest GA release, and the preferred target.</li>
<li><strong>4.1.12:</strong> the latest on the previous stable line.</li>
<li><strong>6.0-alpha3:</strong> pre-GA and not for production.</li>
<li><strong>Java:</strong> Java 11 and 17 are used with Cassandra 5.0.x.</li>
</ul>
<h2 id="cassandra-data-modeling-best-practices-and-partition-key-des">Cassandra Data Modeling Best Practices and Partition Key Design</h2>
<p><a href="https://thecodebuzz.com/what-is-data-modelling-fundamentals-sql-nosql-guidelines/">Design your tables from your queries</a>, not from your entities. A good Cassandra model has three goals: <strong>spread data evenly</strong> with a high-cardinality partition key, <strong>bound partition size</strong>, and <strong>read as few partitions as possible</strong>, ideally one per query.</p>
<h3>Why should a query read only one partition?</h3>
<p>A single-partition read goes to the replicas that own that partition. A multi-partition read makes the coordinator (the node handling the request) contact several nodes and wait for the slowest one. That adds latency variance, so the occasional slow response hurts your tail latency.</p>
<h3>How do you choose the partition key and clustering columns?</h3>
<p>The partition key decides which nodes hold the data. Clustering columns decide the sort order of rows inside that partition. Pick the partition key from the column your query always filters by with equality, and pick clustering columns from the sort or range you need.</p>
<p>Example: a retail app shows &#8220;my orders, newest first&#8221;. The table below shows how the choice maps.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Part</th>
<th>Column</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Partition key</strong></td>
<td><code>customer_id</code></td>
<td>High cardinality, and every query supplies it</td>
</tr>
<tr>
<td><strong>Clustering column</strong></td>
<td><code>order_date DESC</code></td>
<td>Newest orders are read first, with no sorting at query time</td>
</tr>
<tr>
<td><strong>Clustering column</strong></td>
<td><code>order_id</code></td>
<td>Keeps rows unique when two orders share a timestamp</td>
</tr>
</tbody>
</table>
</figure>
<h3>How big should a partition be?</h3>
<p>Keep partitions under about <strong>100 MB</strong> and under about <strong>100,000 items</strong>. The hard practical limit is 2 billion cells per partition, but you should never plan to get near it.</p>
<p>Oversized partitions hurt in several ways:</p>
<ul>
<li><strong>Heavy reads:</strong> the node must scan and merge more data for each request.</li>
<li><strong>Compaction and repair pressure:</strong> large partitions take longer to merge and to compare between replicas.</li>
<li><strong>Hot nodes:</strong> one partition lives on only a few replicas, so one big partition loads those nodes unevenly.</li>
</ul>
<h3>How does bucketing keep partitions bounded?</h3>
<p>Bucketing means adding a column, such as a day or hour, to the partition key so a busy key stops growing forever. One IoT sensor writing every second would otherwise build a single unbounded partition.</p>
<blockquote><p><strong>In plain English:</strong> Think of a ledger where you start a new page every day. You never end up with one endless page, and you can still find any day quickly. The catch is that a question spanning ten days means opening ten pages.</p></blockquote>
<p>Here is a simplified sensor table with a day bucket in the partition key, followed by a single-partition read:</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">CREATE TABLE iot.sensor_readings (
    sensor_id    text,
    day_bucket   date,
    reading_time timestamp,
    temperature  double,
    PRIMARY KEY ((sensor_id, day_bucket), reading_time)
) WITH CLUSTERING ORDER BY (reading_time DESC);

SELECT reading_time, temperature
FROM iot.sensor_readings
WHERE sensor_id = 'sensor-1001'
  AND day_bucket = '2026-10-20'
LIMIT 100;
</code></pre>
<p>✅ One partition, newest readings first, and each partition holds at most one day of data per sensor.</p>
<p>The trade-off: a query over several days now touches several partitions. The application must list the buckets and query each one explicitly (e.g. seven queries for a week). Choose a bucket size so a partition stays within the limits above at your peak write rate.</p>
<h3>What are hot partitions and how do you avoid them?</h3>
<p>A hot partition is one that receives a disproportionate share of traffic. Low-cardinality keys such as <code>country</code> or <code>status</code> create them, because millions of rows share a few key values and land on a few nodes. Use a key with many distinct values, and add a bucket if one value is still very busy.</p>
<h3>Is denormalization normal in Cassandra?</h3>
<p>Yes. Cassandra has no joins, so you store the same data in several tables, one per query (e.g. <code>orders_by_customer</code> and <code>orders_by_date</code>). One write to several tables is cheaper than a read that must gather data from many partitions.</p>
<p>Use a logged batch only to keep those denormalized tables in sync, because it guarantees all writes eventually apply. Don&#8217;t use it for speed. A batch across many partitions adds work on the coordinator.</p>
<h3>Why avoid ALLOW FILTERING and full scans?</h3>
<p><code>ALLOW FILTERING</code> lets a query run without a full partition key, so Cassandra reads many partitions and discards rows that don&#8217;t match. Cost grows with table size, and latency becomes unpredictable. Keep it out of application paths. If you need the query often, create a table for it instead.</p>
<h2 id="indexing-in-cassandra-5-0-sai-secondary-indexes-or-more-tabl">Indexing in Cassandra 5.0: SAI, Secondary Indexes or More Tables</h2>
<p>Use <strong>denormalized tables</strong> for hot, latency-critical queries. Use <strong>Storage-Attached Indexes (SAI)</strong>, new in Cassandra 5.0, for secondary lookups where a partition-key query is not possible. Avoid legacy secondary indexes (2i) in new designs.</p>
<h3>What is SAI in Cassandra 5.0?</h3>
<p>SAI is an index attached to the SSTables of a table. It lets you filter on non-key columns without creating a separate table. The 5.0 feature list also includes a vector data type with similarity functions, which builds on SAI.</p>
<blockquote><p><strong>In plain English:</strong> A denormalized table is a second filing cabinet sorted the way you search. SAI is a small index card stapled to each existing drawer, so you can look things up by another field. The cards help, but you still have to know which drawers to open.</p></blockquote>
<h3>Denormalized table, SAI or legacy index?</h3>
<p>The table below compares the three options across cost and limits.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Option</th>
<th>Read cost</th>
<th>Write cost</th>
<th>Storage</th>
<th>Best fit</th>
<th>Limits</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Denormalized table</strong></td>
<td>Lowest, single partition</td>
<td>One extra write per table</td>
<td>Full copy of the data</td>
<td>Primary, latency-critical queries</td>
<td>App must keep copies in sync</td>
</tr>
<tr>
<td><strong>SAI</strong></td>
<td>Low with a partition restriction, variable without</td>
<td>Index update on write</td>
<td>Index files per SSTable</td>
<td>Secondary filters on non-key columns</td>
<td>Does not replace query-first modeling</td>
</tr>
<tr>
<td><strong>Legacy 2i</strong></td>
<td>Often high, can hit many nodes</td>
<td>Index update on write</td>
<td>Hidden local index table</td>
<td>Rarely a good choice</td>
<td>Poor with high-cardinality columns</td>
</tr>
</tbody>
</table>
</figure>
<h3>What are the limits of SAI?</h3>
<p>An SAI query with no partition key may fan out across nodes, so it adds latency variance. SAI does not remove the need for Cassandra data modeling best practices. Test every SAI query on production-size data before you rely on it, because small test sets hide fan-out cost.</p>
<h3>When should you pick which?</h3>
<ul>
<li><strong>Primary query:</strong> &#8220;orders by customer&#8221; is the main read path, so build it as a table keyed by customer.</li>
<li><strong>Secondary filter:</strong> &#8220;find orders by status in this customer&#8217;s partition&#8221; is fine with SAI plus a partition restriction (e.g. a retail support screen).</li>
<li><strong>Cross-partition search:</strong> Use SAI only if the latency variance is acceptable. Otherwise add another table.</li>
</ul>
<p>Here is a simplified example that indexes the <code>status</code> column and queries it inside one customer partition:</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS shop.orders_by_customer (
  customer_id uuid,
  order_id timeuuid,
  status text,
  total decimal,
  PRIMARY KEY ((customer_id), order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);

CREATE INDEX IF NOT EXISTS orders_status_sai
  ON shop.orders_by_customer (status) USING 'sai';

SELECT order_id, status, total
FROM shop.orders_by_customer
WHERE customer_id = 7b1f3c52-8a6e-4c1d-9d0e-3f2a5b6c7d88
  AND status = 'SHIPPED';</code></pre>
<p>The query returns only the shipped orders for that one customer. The partition key restriction keeps it on the replicas of a single partition, and SAI handles the <code>status</code> filter.</p>
<h2 id="consistency-levels-and-replication-factor-choosing-local-quo">Consistency Levels and Replication Factor: Choosing LOCAL_QUORUM</h2>
<p>For most multi-datacenter production workloads, use <strong>RF=3 per datacenter</strong> with <code>LOCAL_QUORUM</code> for both reads and writes. RF=3 lets a quorum form from two agreeing replicas, tolerates one node failure and allows rolling restarts without downtime.</p>
<h3>What is a consistency level?</h3>
<p>A consistency level is the number of replicas that must confirm a read or write before Cassandra reports success to the client. You choose it per request, so different queries can make different trade-offs.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>Note :</strong> Three people hold a copy of the same record. A consistency level decides how many of them must say &#8220;done&#8221; before you accept the answer, like a committee that needs a majority vote. The analogy stops fitting in one place: Cassandra still sends the write to all copy-holders, it only waits for some of them.</p></blockquote>
<h3>How do QUORUM and LOCAL_QUORUM differ?</h3>
<p><code>QUORUM</code> needs RF/2 + 1 replicas (integer division) across <em>all</em> datacenters. <code>LOCAL_QUORUM</code> needs a majority only in the coordinator&#8217;s datacenter, which is 2 of 3 with RF=3.</p>
<p>With two datacenters at RF=3, there are six replicas in total, so <code>QUORUM</code> needs four. That forces at least one cross-datacenter acknowledgment. <code>LOCAL_QUORUM</code> avoids that WAN latency and keeps working if the other datacenter is completely down. The animated diagram below shows a <code>LOCAL_QUORUM</code> write, including the case where DC2 is unavailable.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cassandra-best-practices-guidelines-diagram-4.gif" alt="Animated LOCAL_QUORUM write: client, DC1 coordinator, replicas R1-R3 in DC1 and R4-R6 in DC2 over a dashed WAN, including a DC2-down case." width="1200" height="675" /><figcaption>With RF=3 per datacenter, LOCAL_QUORUM waits for 2 of 3 DC1 replicas, so the client never waits on the cross-DC link and DC2 can be down.</figcaption></figure>
<h3>Which consistency level fits which use case?</h3>
<p>The table assumes two datacenters with RF=3 each (six replicas in total).</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Level</th>
<th>Replicas needed</th>
<th>Latency</th>
<th>Availability</th>
<th>Typical use</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ONE</strong></td>
<td>1, any DC</td>
<td>Lowest</td>
<td>Highest</td>
<td>Data where stale reads are fine</td>
</tr>
<tr>
<td><strong>LOCAL_ONE</strong></td>
<td>1, local DC</td>
<td>Lowest, no WAN</td>
<td>High</td>
<td>Low-value reads in one DC</td>
</tr>
<tr>
<td><strong>QUORUM</strong></td>
<td>4 of 6</td>
<td>Includes WAN</td>
<td>Medium</td>
<td>Single-DC clusters, cross-DC strictness</td>
</tr>
<tr>
<td><strong>LOCAL_QUORUM</strong></td>
<td>2 of 3 local</td>
<td>Low, no WAN</td>
<td>High</td>
<td>Default for multi-DC production</td>
</tr>
<tr>
<td><strong>EACH_QUORUM</strong> (writes only)</td>
<td>2 of 3 in each DC</td>
<td>High</td>
<td>Low</td>
<td>Rare writes that must reach every DC</td>
</tr>
<tr>
<td><strong>ALL</strong></td>
<td>6 of 6</td>
<td>Highest</td>
<td>Lowest</td>
<td>Almost never</td>
</tr>
</tbody>
</table>
</figure>
<h3>When do you get strong consistency?</h3>
<p>You get strong consistency when read replicas plus write replicas is greater than RF. With RF=3, <code>LOCAL_QUORUM</code> on both sides gives 2 + 2 = 4, which is more than 3, so every read overlaps at least one replica that saw the latest write. This gives read-your-writes within that datacenter.</p>
<p>Know the trade-offs. <code>ALL</code> fails as soon as one replica is down, so one node restart becomes an outage. <code>ONE</code> is fast but can return stale data. Use lower levels only where staleness is acceptable, such as video view counters on a media site. Use <code>LOCAL_QUORUM</code> for order and payment data in a retail system.</p>
<h3>How do you set replication correctly?</h3>
<p>Use <code>NetworkTopologyStrategy</code> with an explicit RF per datacenter, and make sure the snitch reports correct datacenter and rack names. Do not use <code>SimpleStrategy</code> in production multi-datacenter clusters, because it ignores topology and can place all replicas in one rack or datacenter.</p>
<p>The example below creates a keyspace with RF=3 in each datacenter, then sets the session consistency in <code>cqlsh</code>:</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">CREATE KEYSPACE retail
  WITH replication = {
    'class': 'NetworkTopologyStrategy',
    'dc1': 3,
    'dc2': 3
  };

CONSISTENCY LOCAL_QUORUM;</code></pre>
<p>Each datacenter keeps three replicas, and every later read or write in this <code>cqlsh</code> session waits for two local replicas. The names <code>dc1</code> and <code>dc2</code> must match the datacenter names your snitch reports.</p>
<h2 id="transactions-and-lightweight-transactions-paxos-v2-and-accor">Transactions and Lightweight Transactions: Paxos v2 and Accord in Cassandra 6.0</h2>
<p>Cassandra gives you atomicity and isolation <strong>per partition</strong>. For compare-and-set on a single partition it offers lightweight transactions (LWT, written as <code>IF</code> conditions). It is not a general ACID database today.</p>
<h3>When should you use a lightweight transaction?</h3>
<p>Use LWT only where two writers must not both succeed, such as a unique username or an inventory reservation. Paxos, the consensus protocol behind LWT, needs several round trips between replicas, so it costs far more than a normal write. Don&#8217;t make it your default write path.</p>
<blockquote><p><strong>In plain English:</strong> A normal write is like dropping a note in a mailbox. An LWT is like getting every committee member to agree out loud before the note is filed. It is safer, but it takes much longer.</p></blockquote>
<p>Example: a bank checks that an email is unused before opening an account. The check and the insert must happen as one step, as shown below.</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS bank.users_by_email (
  email text PRIMARY KEY,
  user_id uuid,
  created_at timestamp
);

SERIAL CONSISTENCY LOCAL_SERIAL;

INSERT INTO bank.users_by_email (email, user_id, created_at)
VALUES ('asha@example.com', uuid(), toTimestamp(now()))
IF NOT EXISTS;
</code></pre>
<p>The result has an <code>[applied]</code> column. <code>True</code> means the row was inserted. <code>False</code> means the email already exists, and the existing row is returned. Always check this column in your application code. <code>LOCAL_SERIAL</code> limits the Paxos phase to the local datacenter, and <code>SERIAL</code> spans all datacenters.</p>
<h3>Which Paxos settings should you set in production?</h3>
<p>Set <code>paxos_variant</code> to <code>v2</code> together with <code>paxos_state_purging</code> set to <code>repaired</code>. The two settings are independent, so set both deliberately. Test the change in staging and follow the official upgrade notes for your exact version.</p>
<h3>What does Accord in Cassandra 6.0 add?</h3>
<p>Accord adds multi-partition, strict-serializable transactions through <code>BEGIN TRANSACTION</code>. <strong>Cassandra 6.0 is still pre-GA (6.0-alpha3)</strong>, and Accord is beta and off by default. It requires Transactional Cluster Metadata and doesn&#8217;t support counter tables. It also rejects <code>LOCAL_QUORUM</code>, <code>TWO</code> and <code>THREE</code> for Accord operations. Don&#8217;t plan production designs on it yet, because 5.0.9 remains the current GA release.</p>
<h3>Plain writes, logged batch, LWT or Accord: how do they compare?</h3>
<p>This table compares the four options by scope, isolation, cost and status.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Option</th>
<th>Scope</th>
<th>Isolation</th>
<th>Cost</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Plain write</strong></td>
<td>Single partition</td>
<td>Per partition</td>
<td>Lowest</td>
<td>GA</td>
</tr>
<tr>
<td><strong>Logged batch</strong></td>
<td>Multiple tables or partitions</td>
<td>Eventual all-or-nothing, no cross-partition isolation</td>
<td>Extra batchlog writes</td>
<td>GA</td>
</tr>
<tr>
<td><strong>LWT</strong></td>
<td>Single partition, conditional</td>
<td>Serializable per partition</td>
<td>Several round trips</td>
<td>GA</td>
</tr>
<tr>
<td><strong>Accord</strong></td>
<td>Multiple partitions</td>
<td>Strict serializable</td>
<td>Not benchmarked here</td>
<td>Beta, 6.0 pre-GA</td>
</tr>
</tbody>
</table>
</figure>
<h2 id="tombstones-ttl-and-gc-grace-seconds-deleting-data-safely">Tombstones, TTL and gc_grace_seconds: Deleting Data Safely</h2>
<p>A delete in Cassandra writes a <strong>tombstone</strong>, a marker saying the data is deleted, instead of removing it right away. Tombstones are purged only after <code>gc_grace_seconds</code> (default 864000 seconds, 10 days) has passed and only when compaction runs. Until then, reads must scan them, so heavy deletes slow reads.</p>
<blockquote><p><strong>In plain English:</strong> A tombstone is a crossed-out line in a ledger. The old line stays on the page, and the crossing-out is only cleaned up when the ledger is rewritten. Unlike a ledger, Cassandra keeps copies on several nodes, and every copy must see the crossing-out.</p></blockquote>
<h3>Why must repair finish within gc_grace_seconds?</h3>
<p>If a replica misses a delete and the other replicas purge their tombstones, that replica becomes the only holder of the row. The next read or repair copies the old value back, and the deleted row returns as <strong>zombie data</strong>. Repair inside the grace window prevents this.</p>
<p>The animated diagram below shows a delete across three replicas, with the healthy path and the zombie failure path side by side.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cassandra-best-practices-guidelines-diagram-6.gif" alt="Timeline of a delete on replicas A, B, C. Repair before day 10 purges tombstones safely; skipping repair lets row X return as zombie data." width="1200" height="675" /><figcaption>Repair must reach every replica within gc_grace_seconds, or a replica that missed the delete can resurrect the row.</figcaption></figure>
<h3>What creates tombstones?</h3>
<p>These patterns produce most tombstone problems in production:</p>
<ul>
<li><strong>Queue-style tables:</strong> rows are inserted, read and deleted in the same partition, so reads scan many dead entries.</li>
<li><strong>Frequent deletes:</strong> every delete adds a marker that stays for at least the grace period.</li>
<li><strong>Partial deletes in wide partitions:</strong> row or range deletes inside a large partition leave many markers next to live data.</li>
<li><strong>Null writes:</strong> explicitly inserting <code>null</code> writes a tombstone. Leave the column out of the statement, or pass the driver&#8217;s unset value (e.g. <code>UNSET_VALUE</code> in the Python driver).</li>
</ul>
<h3>How do you delete data safely?</h3>
<ol>
<li><strong>Use TTL instead of DELETE:</strong> let data expire on its own schedule. Cassandra 5.0 also extends the maximum TTL, which helps for long retention.</li>
<li><strong>Bucket by time:</strong> put a day or hour bucket in the partition key so old data sits in partitions you stop reading.</li>
<li><strong>Avoid large range deletes:</strong> they leave wide tombstones that every read must check.</li>
<li><strong>Use TWCS for TTL-heavy data:</strong> when data is time-windowed and not updated out of order, whole expired SSTables can be dropped.</li>
</ol>
<p>Example: a retail site keeps cart-activity events for seven days. TTL plus TWCS removes them without any application delete.</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">CREATE TABLE shop.cart_events (
    user_id    uuid,
    day_bucket date,
    event_time timestamp,
    event_type text,
    PRIMARY KEY ((user_id, day_bucket), event_time)
) WITH CLUSTERING ORDER BY (event_time DESC)
  AND default_time_to_live = 604800
  AND compaction = {
    'class': 'TimeWindowCompactionStrategy',
    'compaction_window_unit': 'DAYS',
    'compaction_window_size': 1
  };

INSERT INTO shop.cart_events (user_id, day_bucket, event_time, event_type)
VALUES (uuid(), '2026-10-20', toTimestamp(now()), 'add_to_cart')
USING TTL 86400;</code></pre>
<p>The table default expires rows after seven days, and the insert overrides it with a one-day TTL. Each daily window becomes its own SSTable group that can be dropped as a whole.</p>
<h3>How do you catch tombstone problems early?</h3>
<p>Set the tombstone warn and failure thresholds in <code>cassandra.yaml</code> and review the guardrails available in your version. Check <code>nodetool tablestats</code> for average and maximum tombstones per slice, and watch the logs for tombstone warnings.</p>
<blockquote><p><strong>Warning:</strong> Don&#8217;t lower <code>gc_grace_seconds</code> just to purge faster. A shorter value frees space sooner, but it raises the risk of zombie data unless repair completes more often than the new value. Lower it only after your repair schedule is proven to beat it.</p></blockquote>
<h2 id="choosing-a-compaction-strategy-stcs-lcs-twcs-or-ucs">Choosing a Compaction Strategy: STCS, LCS, TWCS or UCS</h2>
<p><strong>STCS is still the default in Cassandra 5.0</strong>, but the UCS documentation recommends the Unified Compaction Strategy (UCS) for most workloads. UCS can also be configured to behave like the older strategies. If you stay on classic strategies, use TWCS for TTL-heavy time-series data.</p>
<h3>What does compaction do?</h3>
<p>Compaction is the background process that merges SSTables (the immutable data files on disk) to drop overwritten data and expired tombstones. The strategy you pick decides which files get merged and when.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>In plain English:</strong> Think of many small notebooks piling up on a desk. Compaction copies the latest notes into fewer, cleaner notebooks and throws the old ones away. The strategy is the rule for which notebooks you tidy together, and how often.</p></blockquote>
<h3>How do the four strategies compare?</h3>
<p>The animated diagram below walks through the decision for one table, and the table after it compares the strategies in qualitative terms.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cassandra-best-practices-guidelines-diagram-7.gif" alt="Decision tree: TTL time-series leads to TWCS; otherwise Cassandra 5.0+ leads to UCS; else read-heavy leads to LCS, otherwise STCS." width="1200" height="675" /><figcaption>Walk the questions for one table; UCS is the documented recommendation on 5.0+, while STCS remains the default.</figcaption></figure>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Strategy</th>
<th>Best for</th>
<th>Read amplification</th>
<th>Write amplification</th>
<th>Space overhead</th>
<th>Watch out for</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>STCS</strong></td>
<td>Write-heavy, general use</td>
<td>Higher</td>
<td>Lower</td>
<td>Higher</td>
<td>Large compactions need spare disk</td>
</tr>
<tr>
<td><strong>LCS</strong></td>
<td>Read-heavy, strict latency</td>
<td>Lower</td>
<td>Higher</td>
<td>Lower</td>
<td>Heavy write I/O</td>
</tr>
<tr>
<td><strong>TWCS</strong></td>
<td>TTL time-series</td>
<td>Low for recent windows</td>
<td>Lower</td>
<td>Low when expiry works</td>
<td>Out-of-order updates and deletes</td>
</tr>
<tr>
<td><strong>UCS</strong></td>
<td>Most workloads</td>
<td>Tunable</td>
<td>Tunable</td>
<td>Tunable</td>
<td>Check parameters for your version</td>
</tr>
</tbody>
</table>
</figure>
<h3>Why do the strategies behave differently?</h3>
<ul>
<li><strong>LCS:</strong> keeps data in small sorted levels, so a read touches few files. You pay with more write I/O.</li>
<li><strong>STCS:</strong> favors writes by merging files of similar size. Large merges need free disk space while they run.</li>
<li><strong>TWCS:</strong> groups data into time windows and drops a whole SSTable once everything in it has expired. This only works if data isn&#8217;t updated or deleted out of order.</li>
</ul>
<h3>How do you migrate a table to UCS?</h3>
<ol>
<li><strong>Test first:</strong> try the change on a non-production cluster with realistic data.</li>
<li><strong>Change per table:</strong> use <code>ALTER TABLE</code>, one table at a time.</li>
<li><strong>Mimic your current strategy:</strong> start with settings that behave like STCS or LCS, then tune.</li>
<li><strong>Watch the switch:</strong> track pending compactions and disk use while it runs.</li>
</ol>
<p>Parameter names and defaults can change between versions, so confirm them in the official UCS documentation for your release. The <code>T4</code> value below is the STCS-like setting from the 5.0 docs.</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">-- UCS, configured to behave like STCS
ALTER TABLE shop.orders_by_customer
WITH compaction = {
  'class': 'UnifiedCompactionStrategy',
  'scaling_parameters': 'T4'
};

-- TWCS for TTL-heavy time-series data
ALTER TABLE iot.sensor_readings
WITH compaction = {
  'class': 'TimeWindowCompactionStrategy',
  'compaction_window_unit': 'DAYS',
  'compaction_window_size': 1
};</code></pre>
<p>Each statement changes the strategy for one table only. Then check progress on each node:</p>
<p class="code-label"><strong>bash</strong></p>
<pre><code class="language-bash">nodetool compactionstats</code></pre>
<p>This shows pending tasks and running compactions. A pending count that keeps growing after the switch means the node can&#8217;t keep up.</p>
<p>Cassandra 5.0 also adds trie memtables and trie SSTables. They are separate storage features from compaction, and you can evaluate them on their own.</p>
<h2 id="drivers-connection-management-and-security">Drivers, Connection Management and Security</h2>
<p>Use <strong>one long-lived session per application</strong>, prepared statements, token-aware and datacenter-aware load balancing, and an explicitly set local datacenter. Turn on authentication, authorization and TLS from day one, because adding them later means a risky rolling change on a live cluster.</p>
<h3>How should you manage connections?</h3>
<p>Create the <code>Session</code> once at startup and share it across all requests. Opening one per request wastes connections and adds handshake latency. A prepared statement is parsed once on the server, and it lets the driver work out which replicas own the partition.</p>
<blockquote><p><strong>In plain English:</strong> Token-aware routing is like a courier who knows which house holds the parcel and goes straight there, instead of leaving it at a front desk that must pass it on. The &#8220;front desk&#8221; is the coordinator, and skipping it saves one network hop.</p></blockquote>
<p>Set the local datacenter explicitly so traffic never reaches a remote DC by accident. Enable retries and speculative execution (sending a second request when the first is slow) only for idempotent statements, meaning ones that are safe to run twice.</p>
<p>The table below lists the driver settings to check, with the value to use and the reason:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Setting</th>
<th>Recommended</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Local datacenter</strong></td>
<td>Set explicitly (e.g. dc1)</td>
<td>Keeps requests out of remote DCs</td>
</tr>
<tr>
<td><strong>Load balancing</strong></td>
<td>Token-aware over DC-aware</td>
<td>Sends requests to a replica directly</td>
</tr>
<tr>
<td><strong>Prepared statements</strong></td>
<td>Always, for repeated queries</td>
<td>Parsed once, enables token routing</td>
</tr>
<tr>
<td><strong>Retry and idempotence</strong></td>
<td>Mark idempotent statements only</td>
<td>Avoids duplicate non-idempotent writes</td>
</tr>
<tr>
<td><strong>Default consistency level</strong></td>
<td>LOCAL_QUORUM</td>
<td>Matches RF=3 per DC, no cross-DC wait</td>
</tr>
<tr>
<td><strong>Timeouts</strong></td>
<td>Set client timeout above server timeout</td>
<td>Stops hung requests piling up</td>
</tr>
</tbody>
</table>
</figure>
<h3>How do you configure the Python driver?</h3>
<p>Here is a simplified example using <code>ExecutionProfile</code>, the current way to set policies and consistency:</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">import os
import ssl
from cassandra import ConsistencyLevel
from cassandra.auth import PlainTextAuthProvider
from cassandra.cluster import Cluster, ExecutionProfile, EXEC_PROFILE_DEFAULT
from cassandra.policies import DCAwareRoundRobinPolicy, TokenAwarePolicy

profile = ExecutionProfile(
    load_balancing_policy=TokenAwarePolicy(DCAwareRoundRobinPolicy(local_dc="dc1")),
    consistency_level=ConsistencyLevel.LOCAL_QUORUM,
    request_timeout=5.0,
)

cluster = Cluster(
    ["10.0.0.1", "10.0.0.2"],
    auth_provider=PlainTextAuthProvider("app_user", os.environ["CASS_PASSWORD"]),
    ssl_context=ssl.create_default_context(cafile="ca.pem"),
    execution_profiles={EXEC_PROFILE_DEFAULT: profile},
)
session = cluster.connect("shop")  # create once, reuse everywhere

stmt = session.prepare("SELECT * FROM orders_by_customer WHERE customer_id = ?")
stmt.is_idempotent = True
rows = session.execute(stmt, [customer_id])
</code></pre>
<p>✅ One shared session, local DC set, <code>LOCAL_QUORUM</code> by default, TLS on, and a prepared read marked idempotent.</p>
<h3>How do you configure the C# driver?</h3>
<p>With the DataStax C# driver (<code>CassandraCSharpDriver</code>), build the <code>Cluster</code> once and register the session as a singleton:</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">using Cassandra;

var cluster = Cluster.Builder()
    .AddContactPoints("10.0.0.1", "10.0.0.2")
    .WithLocalDatacenter("dc1")
    .WithCredentials("app_user", Environment.GetEnvironmentVariable("CASS_PASSWORD"))
    .WithSSL()
    .Build();

var session = await cluster.ConnectAsync("shop"); // singleton

var prepared = await session.PrepareAsync(
    "SELECT * FROM orders_by_customer WHERE customer_id = ?");

var bound = prepared.Bind(customerId)
    .SetConsistencyLevel(ConsistencyLevel.LocalQuorum)
    .SetIdempotence(true);

RowSet rows = await session.ExecuteAsync(bound);
</code></pre>
<p>The local DC is named, the statement is prepared and bound, and consistency is <code>LocalQuorum</code>.</p>
<h3>How do you secure a Cassandra cluster?</h3>
<p>Enable <code>PasswordAuthenticator</code> and <code>CassandraAuthorizer</code> in <code>cassandra.yaml</code>. Create your own superuser, then disable or drop the default <code>cassandra</code> role. Give each application a role with only the permissions it needs.</p>
<p class="code-label"><strong>CQL</strong></p>
<pre><code class="language-sql">CREATE ROLE analyst WITH PASSWORD = 'use-a-strong-secret' AND LOGIN = true;
GRANT SELECT ON KEYSPACE clinic TO analyst;
</code></pre>
<ul>
<li><strong>TLS:</strong> Enable client-to-node and node-to-node encryption. Use mutual TLS where you control the clients.</li>
<li><strong>Dynamic data masking (5.0):</strong> Hides sensitive column values from roles without unmask rights. Example: mask patient identifiers for analyst roles in a healthcare system.</li>
<li><strong>CIDR authorizer (5.0):</strong> Limits which networks a role can connect from.</li>
<li><strong>Guardrails:</strong> Added in 4.1 and extended in 5.0, they set warn and fail limits that stop risky operations, such as oversized partitions or too many tables.</li>
</ul>
<h2 id="repair-backup-and-restore-best-practices">Repair, Backup and Restore Best Practices</h2>
<p>Run <strong>repair on every node within each gc_grace_seconds window</strong>. With the default of 864000 seconds that means at least once every 10 days, and in practice more often to leave a safety margin. Take scheduled snapshots with retention, enable commit log archiving for point-in-time restore, and test restores regularly.</p>
<h3>What does Cassandra repair do?</h3>
<p>Repair compares the replicas of the same data using Merkle trees (hash trees that summarize ranges of data) and streams any differences between nodes. It fixes data that a replica missed, for example while the node was down.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;"><p><strong>In plain English:</strong> Three people each keep a copy of the same notebook. Repair is the meeting where they compare page summaries, find pages that differ, and fill in the gaps. Unlike real people, the nodes compare hashes first, so they only copy the pages that really differ.</p></blockquote>
<h3>How should you schedule repair?</h3>
<p>Run repair regularly in small pieces instead of rarely in one large job. Follow these rules:</p>
<ul>
<li><strong>Prefer incremental repair:</strong> on versions that support it, it only checks data not yet repaired. Use full repair only when the release notes or a recovery task call for it.</li>
<li><strong>Use -pr on every node:</strong> the primary-range option repairs only the ranges a node owns first, so the same data isn&#8217;t compared three times.</li>
<li><strong>Go node by node:</strong> running repair on all nodes at once adds heavy streaming and compaction load.</li>
<li><strong>Automate it:</strong> open-source Cassandra has no NodeSync equivalent from DSE. Auto Repair (built-in scheduling) arrived in the 5.0.8+ backport and needs explicit opt-in. Otherwise use an external scheduler. Verify against the release notes for your exact version before enabling.</li>
<li><strong>Watch TTL-heavy tables:</strong> expired data becomes tombstones, and a replica that missed one can bring old values back after the purge. This is the same zombie-data risk described in the tombstones section.</li>
</ul>
<p>Here is a simplified example of repairing the primary ranges of one keyspace on the current node:</p>
<p class="code-label"><strong>BASH</strong></p>
<pre><code class="language-bash">nodetool repair -pr shop_ks</code></pre>
<p>✅ Repairs only the ranges this node owns. Repeat on the next node when it finishes.</p>
<h3>Which backup method should you use?</h3>
<p>Use snapshots as the base and add commit log archiving if you need point-in-time recovery. The table compares the four common methods:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Method</th>
<th>What it gives you</th>
<th>Restore path</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>nodetool snapshot</strong></td>
<td>Consistent point-in-time copy of SSTables per node</td>
<td>Copy files back or stream them</td>
<td>Only as fresh as the last run; lives on the same disk until you copy it away</td>
</tr>
<tr>
<td><strong>Incremental backups</strong></td>
<td>Hard links to SSTables flushed since the last snapshot</td>
<td>Restore the snapshot, then add the new files</td>
<td>Files pile up until you move and clean them</td>
</tr>
<tr>
<td><strong>Commit log archiving</strong></td>
<td>Writes between snapshots, for point-in-time restore</td>
<td>Replay logs up to a chosen timestamp</td>
<td>Needs archive and restore commands configured in advance</td>
</tr>
<tr>
<td><strong>sstableloader / nodetool refresh</strong></td>
<td>Loading tools, not backups</td>
<td>Stream (sstableloader) or copy files (refresh)</td>
<td>Needs the schema to exist first</td>
</tr>
</tbody>
</table>
</figure>
<p>Take a named snapshot, then copy it off the node:</p>
<p class="code-label"><strong>BASH</strong></p>
<pre><code class="language-bash">nodetool snapshot -t daily_2026-10-20 shop_ks</code></pre>
<p>Creates a snapshot tagged <code>daily_2026-10-20</code> for every table in <code>shop_ks</code>. Store it off the node, because a snapshot on the same disk does not survive disk loss.</p>
<h3>How do you restore and test a backup?</h3>
<p>Restore the schema first, then the data, then run repair. The animated diagram below shows the full restore flow, step by step.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/cassandra-best-practices-guidelines-diagram-9.gif" alt="Restore flow: backup store feeds a new test cluster with schema, SSTables, and point-in-time commit logs, then repair and row count validation." width="1200" height="675" /></figure>
<p>A restore test loads schema, snapshot SSTables and archived commit logs into a fresh cluster, then repairs and validates row counts against the source.</p>
<ol>
<li><strong>Restore the schema:</strong> apply the saved CQL so tables exist.</li>
<li><strong>Load the data:</strong> stream SSTables with <code>sstableloader</code>, or copy files into the table directory and run <code>nodetool refresh</code>.</li>
<li><strong>Replay commit logs:</strong> stop at the target timestamp for point-in-time recovery.</li>
<li><strong>Repair and validate:</strong> run repair, then compare row counts with the source.</li>
</ol>
<p class="code-label"><strong>BASH</strong></p>
<pre><code class="language-bash">sstableloader -d 10.0.1.11,10.0.1.12 /restore/shop_ks/orders_by_customer</code></pre>
<p>Streams the SSTables in that directory to the live cluster. The path must end in <code>keyspace/table</code>.</p>
<p><strong>A backup that was never restored is untested.</strong> Run the restore on a separate cluster on a schedule, and time it. For example, a retail team can check that last night&#8217;s order tables match production before trusting the backup for peak season.</p>
<h2 id="operations-monitoring-and-upgrades">Operations, Monitoring and Upgrades</h2>
<p>Run a supported Java version, keep OS and disk settings aligned with Cassandra&#8217;s recommendations, watch partition size and pending compactions, and upgrade by rolling one node at a time to a supported release line. The sections below cover each of these.</p>
<h3>Which Java, disk and OS settings should you check?</h3>
<p>Cassandra 5.0.x runs on <strong>Java 11 and Java 17</strong>. Pick one of them and use it on every node.</p>
<ul>
<li><strong>Disks:</strong> Put the commit log and data directories on separate disks where possible, so sequential commit log writes don&#8217;t compete with compaction and reads.</li>
<li><strong>Free space:</strong> Keep free disk for compaction. STCS in particular needs spare room when it merges large SSTables.</li>
<li><strong>Heap:</strong> Start from the JVM options files shipped with your version, not values copied from old blog posts. Those were tuned for other Java and Cassandra releases.</li>
<li><strong>Swap and limits:</strong> Disable swap and raise ulimits and file-descriptor limits as the Cassandra docs recommend.</li>
<li><strong>Clocks:</strong> Run NTP or chrony on every node. Cassandra resolves conflicting writes by timestamp, so a drifting clock can make an older write win.</li>
</ul>
<h3>What should you monitor?</h3>
<p>Start with partition size and compaction backlog, then add latency and GC. The table shows each signal and what to do about it.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Signal</th>
<th>Where to see it</th>
<th>What it means</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Large partitions</strong></td>
<td><code>tablehistograms</code>, <code>tablestats</code> (compacted partition maximum bytes)</td>
<td>A key is growing without a bound</td>
<td>Add a bucket to the partition key</td>
</tr>
<tr>
<td><strong>Pending compactions</strong></td>
<td><code>nodetool compactionstats</code></td>
<td>Compaction can&#8217;t keep up with writes</td>
<td>Review the strategy and disk throughput</td>
</tr>
<tr>
<td><strong>Dropped messages</strong></td>
<td><code>nodetool tpstats</code></td>
<td>Node is overloaded</td>
<td>Find the busy table or slow disk</td>
</tr>
<tr>
<td><strong>Latency percentiles</strong></td>
<td>Metrics, <code>tablehistograms</code></td>
<td>Slow replicas or wide reads</td>
<td>Check partitions, tombstones, GC</td>
</tr>
<tr>
<td><strong>Tombstone warnings</strong></td>
<td>System log</td>
<td>Reads are scanning many deleted cells</td>
<td>Use TTL and bucketing</td>
</tr>
<tr>
<td><strong>Hinted handoff, disk, GC pauses</strong></td>
<td>Metrics and logs</td>
<td>A node was down, or space and memory are tight</td>
<td>Alert early and investigate</td>
</tr>
</tbody>
</table>
</figure>
<p>Use these commands to find oversized partitions:</p>
<p class="code-label"><strong>Bash</strong></p>
<pre><code class="language-bash"># Partition size and cell count percentiles for one table
nodetool tablehistograms retail orders_by_customer

# Look for "Compacted partition maximum bytes" in the output
nodetool tablestats retail.orders_by_customer</code></pre>
<p>The first command shows percentile distributions. The second gives the largest partition on that node.</p>
<h3>How do you upgrade Cassandra safely?</h3>
<p>Upgrade one node at a time, one major version at a time. Versions 3.0 and 3.11 reached end-of-life when 5.0 went GA on September 5, 2024, so plan to leave them. The 4.1 line is at 4.1.12, the 4.0 line is at 4.0.21, and <strong>5.0.9 is the current GA</strong>. Don&#8217;t run 6.0-alpha3 in production.</p>
<blockquote><p><strong>In plain English:</strong> A rolling upgrade is like renovating a hotel one room at a time. Guests keep using the other rooms, so the hotel never closes. It only works if the other rooms are in good shape, which is why you repair first.</p></blockquote>
<ol>
<li><strong>Read NEWS.txt:</strong> Check the target version for required steps and breaking changes.</li>
<li><strong>Snapshot:</strong> Take a snapshot on every node.</li>
<li><strong>Repair and check health:</strong> Finish a repair and confirm all nodes are up.</li>
<li><strong>Freeze schema:</strong> Make no schema changes until every node is upgraded.</li>
<li><strong>Roll each node:</strong> Drain, stop, upgrade, start, then check.</li>
<li><strong>Rewrite SSTables if needed:</strong> Run <code>upgradesstables</code> only when the release notes ask for it.</li>
</ol>
<p>This is the command sequence for one node:</p>
<p class="code-label"><strong>Bash</strong></p>
<pre><code class="language-bash">nodetool snapshot -t pre_upgrade retail
nodetool drain                      # flush memtables, stop accepting writes
sudo systemctl stop cassandra
# install the new Cassandra package or binaries here
sudo systemctl start cassandra
nodetool status                     # wait for UN (Up/Normal) before the next node
nodetool upgradesstables            # only if the release notes require it</code></pre>
<p>Streaming during node operations is faster since 4.0, because zero-copy streaming sends eligible whole SSTables without rewriting them.</p>
<h2 id="common-cassandra-anti-patterns">Common Cassandra Anti-Patterns</h2>
<p>These are the mistakes that most often cause slow reads, hot nodes and data resurrecting. Each row below names the symptom you will see and the fix, with a pointer to the section that explains the reason.</p>
<blockquote><p><strong>In plain English:</strong> An anti-pattern is a habit that looks fine on day one and hurts later. Think of a shop that stacks every receipt in one box: it works for a week, then finding anything takes all afternoon.</p></blockquote>
<p>The table lists 15 Cassandra anti-patterns, the symptom of each, and the fix.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Anti-pattern</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Unbounded partitions (no bucket)</strong></td>
<td>Slow reads, heavy compaction and repair, one busy node</td>
<td>Add a day or hour bucket to the partition key and keep partitions under about 100 MB (see the data modeling section).</td>
</tr>
<tr>
<td><strong>Low-cardinality partition key</strong></td>
<td>A few nodes take most of the traffic</td>
<td>Choose a high-cardinality key such as customer_id instead of country or status.</td>
</tr>
<tr>
<td><strong>Queue-style table with frequent deletes</strong></td>
<td>Reads slow down, tombstone warnings in logs</td>
<td>Use TTL, time bucketing and TWCS (see the tombstones section).</td>
</tr>
<tr>
<td><strong>Explicit null writes</strong></td>
<td>Unexpected tombstones</td>
<td>Leave the column out of the INSERT or use unset values in the driver.</td>
</tr>
<tr>
<td><strong>ALLOW FILTERING in app queries</strong></td>
<td>Latency grows with table size</td>
<td>Create a query table, or use SAI with a partition key restriction.</td>
</tr>
<tr>
<td><strong>Legacy secondary index on a high-cardinality column</strong></td>
<td>Slow, scattered reads</td>
<td>Use a denormalized table or an SAI index (see the indexing section).</td>
</tr>
<tr>
<td><strong>ALL or cross-DC QUORUM by default</strong></td>
<td>Requests fail with one node down, or pay cross-datacenter latency</td>
<td>Use LOCAL_QUORUM with RF=3 per datacenter.</td>
</tr>
<tr>
<td><strong>Rare or skipped repair beyond gc_grace_seconds</strong></td>
<td>Deleted data comes back (zombie data)</td>
<td>Repair every node inside each 10-day window with the default, with a safety margin.</td>
</tr>
<tr>
<td><strong>LWT everywhere</strong></td>
<td>High write latency, contention on hot rows</td>
<td>Keep IF conditions for unique checks and reservations only.</td>
</tr>
<tr>
<td><strong>Large logged batches across many partitions</strong></td>
<td>Coordinator overload, batch size warnings</td>
<td>Use batches only to keep a few denormalized tables in sync.</td>
</tr>
<tr>
<td><strong>One session per request</strong></td>
<td>Connection churn, high latency</td>
<td>Create one long-lived Session and share it.</td>
</tr>
<tr>
<td><strong>No local DC on the driver</strong></td>
<td>Traffic reaches a remote datacenter</td>
<td>Set the local datacenter and use token-aware routing.</td>
</tr>
<tr>
<td><strong>Snapshots never tested</strong></td>
<td>Restore fails when you need it</td>
<td>Restore on a separate cluster on a schedule and time it.</td>
</tr>
<tr>
<td><strong>Staying on EOL 3.x</strong></td>
<td>No fixes, harder upgrade later</td>
<td>Plan a path to 5.0.x, one major version at a time.</td>
</tr>
<tr>
<td><strong>Running 6.0 alpha in production</strong></td>
<td>Unstable, unsupported behavior</td>
<td>Stay on 5.0.9 until 6.0 is GA.</td>
</tr>
</tbody>
</table>
</figure>
<h3>Which anti-patterns should you fix first?</h3>
<p>Start with the ones that can lose or resurrect data: skipped repair and untested snapshots. Then fix partition design, because a bad partition key is the hardest thing to change once data exists. Driver settings and consistency defaults are cheap to correct and worth doing early.</p>
<h2 id="cassandra-production-readiness-checklist">Cassandra Production Readiness Checklist</h2>
<p>Use this Cassandra production checklist as a go-live gate. Each item has a check you can run or verify, so you can mark it done or not done. Any item you can&#8217;t verify is a risk to note before launch.</p>
<h3>Cluster and Driver Setup</h3>
<p>These items confirm the cluster layout and how clients connect to it.</p>
<ol>
<li><strong>Supported version:</strong> every node runs a supported line, preferably 5.0.x (5.0.9 is the latest GA). No 3.x nodes and no 6.0-alpha builds.</li>
<li><strong>Replication:</strong> <code>DESCRIBE KEYSPACE</code> shows NetworkTopologyStrategy with RF=3 in each datacenter.</li>
<li><strong>Driver settings:</strong> the local datacenter is set explicitly and the default consistency is LOCAL_QUORUM. One long-lived session is shared by the application.</li>
<li><strong>Host settings:</strong> the JVM is on Java 11 or 17, swap is off, ulimits are raised, and NTP or chrony reports synced clocks on every node.</li>
</ol>
<h3>Data Model and Storage</h3>
<p>These items catch the design problems that are expensive to fix after launch.</p>
<ol start="5">
<li><strong>Query shape:</strong> each application query reads one partition, and none uses <code>ALLOW FILTERING</code>.</li>
<li><strong>Partition size:</strong> <code>nodetool tablehistograms</code> and <code>tablestats</code> show compacted partition maximum bytes under about 100 MB.</li>
<li><strong>Bounded keys:</strong> no partition key grows forever. A time bucket is in the key wherever volume is unbounded.</li>
<li><strong>Table settings:</strong> every table has a deliberate TTL and compaction strategy (UCS, or TWCS for TTL-heavy data).</li>
</ol>
<h3>Recovery and Upgrades</h3>
<p>These items prove you can recover from failure and keep deleted data deleted.</p>
<ol start="9">
<li><strong>Repair:</strong> the schedule covers every node within <code>gc_grace_seconds</code> (864000 seconds by default), with a safety margin.</li>
<li><strong>Backups:</strong> snapshots run on a schedule with retention, are copied off the node, and commit log archiving is on.</li>
<li><strong>Restore test:</strong> a restore into a separate cluster has been completed and timed, and the time is written down.</li>
<li><strong>Upgrade runbook:</strong> a written rolling-upgrade procedure exists and has been rehearsed in staging.</li>
</ol>
<h3>Security and Monitoring</h3>
<p>These items keep the cluster protected and give you early warning.</p>
<ol start="13">
<li><strong>Access control:</strong> authentication, authorization and TLS (client-to-node and node-to-node) are enabled, and the default <code>cassandra</code> superuser is replaced.</li>
<li><strong>Guardrails:</strong> warn and fail limits are configured, for example for partition size and table count.</li>
<li><strong>Alerts:</strong> alerts fire on latency percentiles, pending compactions, disk usage and dropped messages, and each alert has a named owner.</li>
</ol>
<h2 id="key-takeaways">Key takeaways</h2>
<p>These are the points to remember from this guide before you take a Cassandra cluster to production:</p>
<ul>
<li><strong>Model from queries:</strong> design one table per query, and accept denormalization. Cassandra has no joins, so one write to several query tables is cheaper than reads that need many partitions.</li>
<li><strong>Bound your partitions:</strong> keep each partition under about 100 MB and about 100,000 items. Add a time bucket (e.g. day or hour) to the partition key for busy keys, and check the real size with <code>nodetool tablehistograms</code> and <code>tablestats</code>.</li>
<li><strong>Use LOCAL_QUORUM with RF=3:</strong> set <code>NetworkTopologyStrategy</code> with RF=3 per datacenter, and use LOCAL_QUORUM for reads and writes. This gives read-your-writes inside the datacenter, tolerates one node down and avoids cross-DC latency.</li>
<li><strong>Prefer TTL over deletes:</strong> explicit deletes and null writes create tombstones that slow reads. Use TTL, time bucketing and TWCS for time-series data instead.</li>
<li><strong>Finish repair inside gc_grace_seconds:</strong> the default is 864000 seconds (10 days). If repair is late, a replica that missed a delete can bring the old data back. Schedule it with the opt-in Auto Repair (5.0.8+) or an external scheduler.</li>
<li><strong>Test your restores:</strong> schedule snapshots with retention, store them off the node, archive commit logs for point-in-time recovery, and restore into a separate cluster on a schedule. A backup that was never restored is untested.</li>
<li><strong>Pick compaction and indexes deliberately:</strong> STCS is still the 5.0 default, but UCS is recommended for most workloads, and TWCS suits TTL-heavy data. SAI helps with secondary lookups, but it does not replace query-first modeling. Test both on production-size data first.</li>
<li><strong>Stay on supported, GA releases:</strong> 5.0.9 is the current GA release, and 3.x is end-of-life. Treat Accord and Cassandra 6.0 (still 6.0-alpha3) as pre-GA, and keep them out of production designs.</li>
</ul>
<p>That&#8217;s all! Happy coding!</p>
<p>Did this Cassandra best practices guide help you? Please share your questions and your own experience in the comments below.</p>
<p>Bookmark this page for your next production review, and share it with your team. Subscribe to TheCodeBuzz for more database best practices guides.</p>
<h2 id="faq">FAQ</h2>
<h3><strong>What is the ideal partition size in Cassandra?</strong></h3>
<p>Keep each partition under about 100 MB and under about 100,000 items. The hard practical limit is 2 billion cells per partition, but you should stay far below it. Check real sizes with <code>nodetool tablehistograms</code> and <code>nodetool tablestats</code> (look at the compacted partition maximum bytes), and add a time bucket to the partition key if a partition keeps growing.</p>
<h3><strong>Which consistency level should I use in production, QUORUM or LOCAL_QUORUM?</strong></h3>
<p>Use LOCAL_QUORUM for multi-datacenter clusters, with RF=3 in each datacenter. It needs a majority (2 of 3) in the coordinator&#8217;s datacenter only, so it avoids cross-DC latency and keeps working if another datacenter is down. QUORUM counts replicas across all datacenters, and EACH_QUORUM is supported for writes only.</p>
<h3><strong>How often should I run nodetool repair, and why must it finish within gc_grace_seconds?</strong></h3>
<p>Run repair on every node at least once within each gc_grace_seconds window, which is 10 days (864000 seconds) by default. In practice, repair more often to leave a safety margin. If a replica missed a delete and the tombstone is purged on the other replicas, the old value can come back as zombie data. Cassandra 5.0.8 and later include Auto Repair, which needs explicit opt-in, so check the release notes for your exact version before enabling it.</p>
<h3><strong>Should I use Storage-Attached Indexes (SAI) or denormalize into more tables?</strong></h3>
<p>Denormalize into tables for your hot, latency-critical queries, and use SAI for secondary lookups where a partition-key query is not possible. Avoid legacy secondary indexes (2i) in new designs. An SAI query without a partition key restriction can fan out across nodes and add latency variance, so test it on production-size data.</p>
<h3><strong>Does Cassandra support ACID transactions, and what changes in Cassandra 6.0?</strong></h3>
<p>Not fully. Cassandra gives atomicity and isolation per partition, and lightweight transactions (<code>IF</code> conditions) give compare-and-set on a single partition. For Paxos-based LWT, the recommended production setup is <code>paxos_variant</code> v2 with <code>paxos_state_purging</code> set to repaired. Cassandra 6.0 adds Accord, which offers multi-partition, strict-serializable transactions through <code>BEGIN TRANSACTION</code>. Accord is beta and off by default, and 6.0 is still pre-GA (6.0-alpha3), so 5.0.9 remains the current GA release and you should not plan production designs on Accord yet.</p>
<h2 id="sources">Sources and further reading</h2>
<p>&nbsp;</p>
<ul>
<li><a href="https://cassandra.apache.org/_/download.html" target="_blank" rel="noopener">Apache Cassandra: Downloading Cassandra</a> (Apache Cassandra project)</li>
<li><a href="https://cassandra.apache.org/_/blog/Apache-Cassandra-5.0-Announcement.html" target="_blank" rel="noopener">Announcing Apache Cassandra® 5.0</a> (Apache Cassandra project)</li>
<li><a href="https://cassandra.apache.org/doc/stable/cassandra/new/" target="_blank" rel="noopener">New Features in Apache Cassandra 5.0</a> (Apache Cassandra project)</li>
</ul>
<p>&nbsp;</p><p>The post <a href="https://thecodebuzz.com/cassandra-best-practices-guidelines/">Cassandra Best Practices: Data Modeling, Consistency, Operations</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/cassandra-best-practices-guidelines/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Agentic Workflow Best Practices: Architecture and Checklist</title>
		<link>https://thecodebuzz.com/agentic-workflow-best-practices/</link>
					<comments>https://thecodebuzz.com/agentic-workflow-best-practices/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Sun, 18 Oct 2026 17:50:44 +0000</pubDate>
				<category><![CDATA[AI Engineering Best Practices]]></category>
		<category><![CDATA[Agentic AI]]></category>
		<category><![CDATA[AI Agents]]></category>
		<category><![CDATA[LLM Workflows]]></category>
		<category><![CDATA[MCP]]></category>
		<category><![CDATA[OWASP]]></category>
		<category><![CDATA[Production AI]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30881</guid>

					<description><![CDATA[<p>Agentic workflow best practices for architecture, agent loops, security, tracing and evaluation, with Python and C# code and a production checklist.</p>
<p>The post <a href="https://thecodebuzz.com/agentic-workflow-best-practices/">Agentic Workflow Best Practices: Architecture and Checklist</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>This guide covers agentic workflow best practices: architecture, the stages of a run, and a production checklist. It&#8217;s written for engineers and architects building AI agents and LLM features, and for managers, founders and students who want to know how these systems stay under control. Code is in Python and C# (.NET).</p>
<p>Note- Version-specific facts are dated 2026-10-17.</p>
<p>&nbsp;</p>
<p>The main principle is simple: use the smallest thing that works. A single call to an LLM with document lookup (retrieval) and a few examples is often enough. Agents trade latency and cost for flexibility. In simple terms, an LLM (large language model) is the text-generating AI behind chat assistants. In simple terms, an agentic workflow is a multi-step AI task in which a model uses tools and data while your code sets the steps, limits and checks. In simple terms, an agent is a system where the model itself decides the next step and which tool to use.</p>
<p>Next come the workflow-versus-agent choice, design patterns, the agent loop, durable execution, human approval, security with the OWASP Agentic Top 10, tracing, evaluation, and cost and latency.</p>
<blockquote>
<p><strong>Version note:</strong> As of 2026-10-17, this guide targets LangGraph 1.2.16, OpenAI Agents SDK 0.23.1, Microsoft Agent Framework 1.0, MCP specification revision 2026-07-28 and the OWASP Top 10 for Agentic Applications 2026.</p>
<p>&nbsp;</p>
<p><strong>In this guide</strong></p>
</blockquote>
<ul>
<li><a href="#in-plain-english-what-is-an-agentic-workflow">In Plain English: What Is an Agentic Workflow?</a></li>
<li><a href="#agentic-workflow-best-practices-at-a-glance">Agentic Workflow Best Practices at a Glance</a></li>
<li><a href="#agentic-ai-workflow-architecture-where-it-fits-in-the-pipeli">Agentic AI Workflow Architecture: Where It Fits in the Pipeline</a></li>
<li><a href="#agent-workflow-vs-autonomous-agent-choosing-patterns">Agent Workflow vs Autonomous Agent: Choosing Patterns</a></li>
<li><a href="#build-the-agent-loop-state-limits-durability-and-approvals">Build the Agent Loop: State, Limits, Durability and Approvals</a></li>
<li><a href="#secure-agentic-workflows-with-owasp-agentic-top-10-controls">Secure Agentic Workflows with OWASP Agentic Top 10 Controls</a></li>
<li><a href="#observe-evaluate-and-tune-agents-tracing-evals-cost-and-late">Observe, Evaluate and Tune Agents: Tracing, Evals, Cost and Latency</a></li>
<li><a href="#common-anti-patterns-in-agentic-workflows">Common Anti-Patterns in Agentic Workflows</a></li>
<li><a href="#production-readiness-checklist-for-ai-agents">Production Readiness Checklist for AI Agents</a></li>
<li><a href="#glossary-in-plain-english">Glossary in Plain English</a></li>
<li><a href="#key-takeaways">Key Takeaways</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2> </h2>
<h2 id="in-plain-english-what-is-an-agentic-workflow">What Is an Agentic Workflow?</h2>
<p><strong>Tip</strong>: what is an agentic workflow? Think of a capable assistant working through a to-do list. The assistant can look things up, use tools and check their own work, while a manager sets a time limit and a budget.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> A human assistant who gets stuck usually notices. Here the &#8220;assistant&#8221; is software that writes likely-sounding answers, so it can be confidently wrong. That is why it needs checks a human assistant would not.</p>
</blockquote>
<p>The assistant repeats one loop: think, do one thing, look at the result, then decide whether to continue, stop or ask a person. The animated diagram below shows this loop, including the step limit and the hand-off to a person.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/agentic-workflow-best-practices-diagram-1.gif" alt="Flow from a request to plan, tool, result check and a Done decision that loops back, answers, or asks a person, with a step limit gauge." width="1200" height="675" /><figcaption>The assistant repeats plan, act, check until done, within a step limit, and hands risky or unclear cases to a person.</figcaption></figure>
<p>These are the four terms you need for the rest of the article:</p>
<ul>
<li><strong>Agentic workflow:</strong> In simple terms, a task where an AI repeats the think, act, check loop until the job is done.</li>
<li><strong>Agent:</strong> In simple terms, the AI part that chooses its own next step.</li>
<li><strong>LLM:</strong> In simple terms, a large language model, the program that reads and writes text.</li>
<li><strong>Tool call:</strong> In simple terms, the AI asking your code to run an action, such as a search or a database lookup.</li>
</ul>
<p><strong>Why it matters:</strong> without limits, state and checks, an agent can loop forever, spend money, or take a wrong action nobody notices.</p>
<h3>Workflow or Agent: Who Decides the Steps?</h3>
<p>The difference is who picks the path. In a <strong>workflow</strong>, your code decides the steps and the model fills them in. In an <strong>agent</strong>, the model decides the steps and the tools to use. This difference drives the agent workflow vs autonomous agent choice covered later.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/agentic-workflow-best-practices-photo-1.jpg" alt="innovation, business, businessman" width="1200" height="675" /></p>
<figcaption>Image by <a href="https://pixabay.com/users/jarmoluk-143740/" target="_blank" rel="noopener nofollow">jarmoluk</a> from <a href="https://pixabay.com/photos/innovation-business-businessman-561388/" target="_blank" rel="noopener nofollow">Pixabay</a></figcaption>
</figure>
<h2 id="agentic-workflow-best-practices-at-a-glance">Agentic Workflow Best Practices at a Glance</h2>
<p>The rules below run in order from architecture decisions to day-to-day operations, so you can read the table top to bottom as a design sequence.</p>
<p>Four terms appear in the table. In simple terms, an <strong>idempotent</strong> call gives the same result when repeated. <strong>Least agency</strong> means giving an agent only the autonomy and permissions its task needs. <strong>Model tiering</strong> means using a small, cheap model for easy steps and a larger one for hard steps. A <strong>trajectory</strong> is the full sequence of steps and tool calls in one run.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> an idempotent action is like a light switch marked &#8220;set to ON&#8221;. Pressing it twice leaves the light on. A toggle switch is not idempotent, because pressing it twice undoes the first press. This is why a retry is safe for one and risky for the other.</p>
</blockquote>
<p>This table shows what to do and what to avoid in each area of an agentic workflow:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Area</th>
<th>Do this</th>
<th>Avoid this</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Architecture choice</strong></td>
<td>Pick the simplest design that meets the task.</td>
<td>Using an autonomous agent where a fixed workflow works.</td>
</tr>
<tr>
<td><strong>Patterns</strong></td>
<td>Use named agentic workflow design patterns with clear roles.</td>
<td>Adding agents without distinct tools or permissions.</td>
</tr>
<tr>
<td><strong>State</strong></td>
<td>Keep an explicit state object outside the prompt.</td>
<td>Holding state only in the conversation text.</td>
</tr>
<tr>
<td><strong>Tool design</strong></td>
<td>Use typed schemas, single-purpose tools, idempotent writes and input validation.</td>
<td>Free-text arguments and &#8220;do everything&#8221; tools.</td>
</tr>
<tr>
<td><strong>Loop limits and budgets</strong></td>
<td>Cap steps, time and cost per run.</td>
<td>Open-ended loops.</td>
</tr>
<tr>
<td><strong>Retries and durability</strong></td>
<td>Checkpoint each step and retry only safe calls.</td>
<td>Restarting from zero or retrying non-idempotent writes.</td>
</tr>
<tr>
<td><strong>Human approval</strong></td>
<td>Gate risky actions by risk level.</td>
<td>Approving every step.</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td>Use least agency and task-scoped credentials. Treat tool output and retrieved content as untrusted data.</td>
<td>Broad long-lived keys, or obeying text found in tool results.</td>
</tr>
<tr>
<td><strong>Observability</strong></td>
<td>Trace each run as one tree.</td>
<td>Logging only the final answer.</td>
</tr>
<tr>
<td><strong>Evaluation</strong></td>
<td>Score trajectories and keep a regression suite.</td>
<td>Checking only final-answer quality.</td>
</tr>
<tr>
<td><strong>Cost</strong></td>
<td>Use model tiering, prompt caching and context compression.</td>
<td>Sending the largest model the full history on every step.</td>
</tr>
<tr>
<td><strong>Dependency pinning</strong></td>
<td>Pin framework, SDK and protocol versions.</td>
<td>Floating versions on fast-moving or 0.x packages.</td>
</tr>
</tbody>
</table>
</figure>
<p>Each row is explained with its trade-off in the sections below.</p>
<h2 id="agentic-ai-workflow-architecture-where-it-fits-in-the-pipeli">Agentic AI Workflow Architecture: Where It Fits in the Pipeline</h2>
<p>The agentic layer sits between retrieval and the user. It calls the knowledge built during ingestion, the tools and the LLM, while evaluation and operations wrap around the whole thing. The animated diagram below shows a document flowing into storage, then a user question driving the agent.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/agentic-workflow-best-practices-diagram-3.gif" alt="Pipeline from ingestion to vector store and retrieval, with agent control calling the LLM, retrieval and tools, over an evaluation band." width="1200" height="675" /></p>
<figcaption>The agent layer sits after retrieval and calls retrieval, tools and the LLM, while evaluation and operations record traces for every step.</figcaption>
</figure>
<h3>What does each pipeline stage do for the agent?</h3>
<p>Each stage prepares something the agent uses at run time. Weak work early in the pipeline shows up later as poor agent decisions.</p>
<ul>
<li><strong>Ingestion:</strong> loads and cleans source documents.</li>
<li><strong>Chunking:</strong> splits documents into pieces. In simple terms, a chunk is one small passage cut from a longer document.</li>
<li><strong>Embeddings:</strong> turn each chunk into numbers. In simple terms, an embedding is a list of numbers that places a text on a map of meaning.</li>
<li><strong>Vector store:</strong> keeps those embeddings searchable. In simple terms, a vector store is a database that finds texts by meaning instead of exact words.</li>
<li><strong>Retrieval:</strong> fetches the best-matching chunks for a question. In simple terms, retrieval is looking up the most relevant passages before answering.</li>
<li><strong>LLM:</strong> reads the prompt and writes text or picks a tool. It can only use what fits in its context window. In simple terms, the context window is the amount of text the model can read at once.</li>
<li><strong>Agent or workflow control:</strong> decides the next step, enforces limits and stores state.</li>
<li><strong>Evaluation and operations:</strong> record traces, budgets and alerts, and test behaviour.</li>
</ul>
<p>Retrieval is <strong>one tool the agent can call</strong>, next to APIs and databases. If it returns the wrong chunks, every later step reasons from bad evidence.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> Think of a research assistant with a library built in advance. The pipeline stocks and indexes the shelves. The agent decides when to visit them, which other tools to use, and when to stop. The analogy stops at one point: the assistant is a language model, so it can misread a good source with confidence.</p>
</blockquote>
<h3>Which orchestration approach should you choose?</h3>
<p>Choose by <strong>failure semantics</strong> first: what happens on a crash, a timeout or bad tool output. Framework style comes second. The table below compares the four kinds of tooling, with facts dated 2026-10-17.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Kind</th>
<th>What it gives you</th>
<th>Dated facts</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Agent frameworks</strong></td>
<td>Control flow encoded as graphs or handoffs</td>
<td>LangGraph 1.2.16 (PyPI, Oct 16, 2026) has type-safe streaming, graceful shutdown and human-in-the-loop interrupts. Checkpoints use SQLite or memory for development and PostgreSQL for production. OpenAI Agents SDK is 0.23.1 (Oct 2, 2026), still 0.x, with the API changing between minor versions. Microsoft Agent Framework 1.0 reached GA on Apr 3, 2026 for .NET and Python, with MCP and A2A interoperability.</td>
</tr>
<tr>
<td><strong>Durable workflow engines</strong></td>
<td>Retries, checkpoints and crash recovery</td>
<td>Best when runs are long or side effects are costly.</td>
</tr>
<tr>
<td><strong>Managed agent platforms</strong></td>
<td>Hosted harness, sessions and sandboxes</td>
<td>OpenAI Agents API has been in public beta since Sep 10, 2026, with durable sessions, MCP servers and sandboxes.</td>
</tr>
<tr>
<td><strong>Plain code, no framework</strong></td>
<td>Full control, no dependency</td>
<td>Enough for a short fixed chain.</td>
</tr>
</tbody>
</table>
</figure>
<p>Pick the <strong>least autonomous architecture</strong> that meets the task. A short fixed chain does not need a framework. Pin exact versions in production, because LangGraph ships frequent patch releases and the OpenAI SDK is pre-1.0.</p>
<h2 id="agent-workflow-vs-autonomous-agent-choosing-patterns">Agent Workflow vs Autonomous Agent: Choosing Patterns</h2>
<p>Start with a single LLM call plus retrieval and examples. Move to a fixed workflow if that is not enough. Use an autonomous agent only when the steps cannot be known in advance.</p>
<p>Agentic systems usually trade latency and cost for better task performance, so each step up must earn its place. A study of production agents found that 16 of 20 case studies (80%) used predefined structured workflows, chosen for controllability and human oversight.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> A workflow is a recipe card: the steps are written down and the cook follows them. An agent is a chef who decides the next step after tasting the dish. The analogy stops there: the chef here is a language model, so it can be confidently wrong and needs limits and checks.</p>
</blockquote>
<h3>Which design should you choose?</h3>
<p>Apply these decision rules in order:</p>
<ol>
<li><strong>Steps known:</strong> use a fixed workflow, where your code decides the order.</li>
<li><strong>Path depends on what the model finds:</strong> use an agent, but only if the task value justifies the extra cost and latency.</li>
<li><strong>Mistake is costly or irreversible:</strong> add human approval gates before the risky action.</li>
</ol>
<p>The animated diagram below walks a new task through these questions.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/agentic-workflow-best-practices-diagram-4.gif" alt="Decision tree: a task passes four questions, ending at a single call, fixed workflow, rethink, agent with budgets, or agent with approval gate." width="1200" height="675" /></p>
<figcaption>Each step up from a single call must earn its extra latency and cost; irreversible actions get a human approval gate.</figcaption>
</figure>
<h3>Which agentic workflow design patterns exist?</h3>
<p>In simple terms, an orchestrator is a coordinator that splits a task and hands the parts to workers. This table compares the common agentic workflow design patterns.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Pattern</th>
<th>Use when</th>
<th>Cost or latency trade-off</th>
<th>Main risk</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Prompt chaining</strong></td>
<td>A task splits into fixed sequential steps</td>
<td>One call per step, so latency adds up</td>
<td>An early error passes to later steps</td>
</tr>
<tr>
<td><strong>Routing</strong></td>
<td>Input types need different handling</td>
<td>One cheap classification call</td>
<td>Misrouted requests</td>
</tr>
<tr>
<td><strong>Parallelization</strong></td>
<td>Subtasks are independent</td>
<td>Lower latency, same or higher token cost</td>
<td>Merging conflicting results</td>
</tr>
<tr>
<td><strong>Orchestrator-workers</strong></td>
<td>Subtasks are not known until runtime</td>
<td>Extra planning call plus worker calls</td>
<td>Poor task splitting</td>
</tr>
<tr>
<td><strong>Evaluator-optimizer</strong></td>
<td>Clear quality criteria allow iterative improvement</td>
<td>Each round adds calls</td>
<td>Endless refinement without a round limit</td>
</tr>
<tr>
<td><strong>Plan-and-execute</strong></td>
<td>A plan can be written first, then run</td>
<td>One planning call, cheaper execution steps</td>
<td>A stale plan when reality changes</td>
</tr>
<tr>
<td><strong>Supervisor and workers</strong></td>
<td>Roles need different tools or permissions</td>
<td>Every handoff adds cost and delay</td>
<td>Unclear ownership between agents</td>
</tr>
</tbody>
</table>
</figure>
<h3>When should you split into multiple agents?</h3>
<p>Split only when roles need different tools, permissions or context. More agents multiply cost, latency and failure points, because every handoff is another place for a call to fail or for context to be lost. In multi-agent orchestration patterns, one agent with well-scoped tools is the default to beat.</p>
<p><strong>Use Case:</strong> A retail returns-triage workflow routes each request to refund, exchange or fraud review. The categories are known, so a router plus fixed branches is enough, and refunds above a set amount wait for human approval.</p>
<p><strong>Use Case:</strong> A finance research task uses orchestrator-workers. The orchestrator picks which filings and news to read based on the question, workers summarise each source, and an analyst reviews the draft before it is shared.</p>
<h2 id="build-the-agent-loop-state-limits-durability-and-approvals">Build the Agent Loop: State, Limits, Durability and Approvals</h2>
<p>An operable agent loop has an <strong>explicit state object</strong> (plan, observations, remaining budget), typed tool schemas, a retry policy, a loop limit, a per-step timeout, a cost budget and clear termination outcomes: succeeded, gave up or escalated. The CoreWeave production guide lists these as the minimum for knowing what an agent did and why it stopped. The same pieces are the base of <em>production AI agents checklist</em> items such as limits, retries and approvals.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> Think of a contractor with a work order: a task list, a spending cap, a deadline and a rule to phone the client before any big purchase. The analogy stops at trust. A language model can be confidently wrong, so your code enforces the cap, the deadline and the phone call. It does not rely on the model to respect them.</p>
</blockquote>
<p>The animated diagram below shows one pass of the loop, the state card that changes on every pass, and the three ways the loop can end.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/agentic-workflow-best-practices-diagram-5.gif" alt="Agent loop of plan, call tool, observe and check limits, with a state card, a retry path, and exits to success, dead-letter queue or human approval." width="1200" height="675" /></p>
<figcaption>Your code, not the model, enforces limits, retries and approvals; every pass is checkpointed and ends as succeeded, gave up or escalated.</figcaption>
</figure>
<h3>Which terms does the loop use?</h3>
<ul>
<li><strong>Agent state:</strong> In simple terms, it is the notebook the agent carries between steps: the goal, the plan, what it has seen and how much budget is left.</li>
<li><strong>Token:</strong> In simple terms, it is a small piece of text (a word or part of a word) that models read and write. Cost is usually counted in tokens.</li>
<li><strong>Temperature:</strong> In simple terms, it is a setting for how adventurous the model&#8217;s word choices are. Set it low for tool selection, because you want the same situation to give the same tool and well-formed arguments, not creative variation.</li>
<li><strong>Idempotency:</strong> In simple terms, doing the same action twice has the same effect as doing it once. Reading an order is idempotent. Charging a card is not, unless you send an idempotency key the payment service can recognise.</li>
<li><strong>Checkpoint:</strong> In simple terms, it is a saved copy of the state after a step, like a game save, so a crash resumes instead of restarting.</li>
<li><strong>Superstep:</strong> In simple terms, it is one round of a graph-based framework in which the ready steps run and the state is saved. LangGraph uses this term.</li>
</ul>
<h3>How do you set loop limits and budgets?</h3>
<p>The research gives no universal numbers for step limits, timeouts or cost caps, so treat every default as a placeholder to be tuned. Replay a set of real tasks and record the step-count distribution and the cost per successful run. Set the loop limit slightly above a high percentile of successful runs, then review every run that hits it. Those runs show whether the limit is too tight or the agent is genuinely stuck.</p>
<h3>How do you make the loop survive failures?</h3>
<p>Checkpoint after every step. Retry only idempotent calls, or writes that carry an idempotency key. Classify each error before deciding what to do:</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Error class</th>
<th>Example</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Retryable</strong></td>
<td>Timeout, dropped connection</td>
<td>Retry with backoff, up to a fixed count</td>
</tr>
<tr>
<td><strong>Fatal</strong></td>
<td>Bug, unknown tool, retries exhausted</td>
<td>Stop and give up</td>
</tr>
<tr>
<td><strong>Needs a human</strong></td>
<td>Permission denied, risky action declined</td>
<td>Escalate</td>
</tr>
</tbody>
</table>
</figure>
<p>Send exhausted tasks, with their saved state, to a <strong>dead-letter queue</strong>. In simple terms, that is a holding tray for jobs that failed, kept so a person can inspect and replay them. LangGraph&#8217;s graceful shutdown stops a run after the current superstep and saves a resumable checkpoint. Its checkpoint backends are in-memory or SQLite for development and PostgreSQL for production.</p>
<h3>How do you add human approval?</h3>
<p>For <em>human-in-the-loop AI agents</em>, interrupt before risky tools such as payments, deletes and outbound emails. Show the proposed arguments, then resume on approve or edit. Gate by risk level only, because approval fatigue is real: reviewers who see every action start approving without reading.</p>
<h3>Python example: a bounded agent loop</h3>
<p>Here&#8217;s a simplified example using the standard library and pydantic v2. The <code>call_llm</code> function is a stub you replace, and you can add custom logic for planning, logging and storage.</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable

from pydantic import BaseModel, Field, ValidationError


class Outcome(Enum):
    SUCCEEDED = "succeeded"
    GAVE_UP = "gave_up"
    ESCALATED = "escalated"


@dataclass
class AgentState:
    goal: str
    run_id: str = field(default_factory=lambda: uuid.uuid4().hex)
    plan: list[str] = field(default_factory=list)
    observations: list[str] = field(default_factory=list)
    step: int = 0
    tokens_used: int = 0
    cost_usd: float = 0.0
    answer: str | None = None


class Decision(BaseModel):
    action: str  # "tool" or "finish"
    tool: str | None = None
    args: dict[str, Any] = Field(default_factory=dict)
    answer: str | None = None
    tokens: int = 0
    cost_usd: float = 0.0


class RefundArgs(BaseModel):
    order_id: str = Field(pattern=r"^ORD-\d{6}$")
    amount: float = Field(gt=0, le=500)


def issue_refund(args: RefundArgs, idempotency_key: str) -&gt; str:
    # Replace: send idempotency_key to the payment API so a retry cannot pay twice.
    return f"refund of {args.amount} queued for {args.order_id}"


@dataclass
class Tool:
    args_model: type[BaseModel]
    func: Callable[[Any, str], str]
    risky: bool = False
    idempotent: bool = False  # True = read-only, or a write that uses the key


RETRYABLE = (TimeoutError, ConnectionError)


def call_llm(state: AgentState) -&gt; Decision:
    # Stub: call your model here (low temperature) and return a Decision.
    raise NotImplementedError


def call_tool(tool: Tool, args: BaseModel, key: str, retries: int, deadline: float) -&gt; str:
    attempts = retries + 1 if tool.idempotent else 1
    for attempt in range(attempts):
        try:
            return tool.func(args, key)
        except RETRYABLE:
            if attempt == attempts - 1 or time.monotonic() &gt;= deadline:
                raise
            time.sleep(min(0.5 * 2**attempt, 5))
    raise RuntimeError("unreachable")


def run_agent(
    state: AgentState,
    tools: dict[str, Tool],
    approve: Callable[[str, BaseModel], BaseModel | None],
    save_checkpoint: Callable[[AgentState], None],
    *,
    max_steps: int = 8,         # placeholders: tune from replayed real tasks
    max_cost_usd: float = 0.50,
    timeout_s: float = 60.0,
    max_retries: int = 2,
) -&gt; Outcome:
    deadline = time.monotonic() + timeout_s
    while (state.step &lt; max_steps and state.cost_usd &lt; max_cost_usd
           and time.monotonic() &lt; deadline):
        decision = call_llm(state)
        state.step += 1
        state.tokens_used += decision.tokens
        state.cost_usd += decision.cost_usd

        if decision.action == "finish":
            state.answer = decision.answer
            save_checkpoint(state)
            return Outcome.SUCCEEDED

        tool = tools.get(decision.tool or "")
        if tool is None:
            state.observations.append(f"unknown tool: {decision.tool}")
        else:
            try:
                args = tool.args_model.model_validate(decision.args)
            except ValidationError as exc:
                state.observations.append(f"invalid arguments: {exc}")
            else:
                if tool.risky:
                    args = approve(decision.tool, args)  # approve, edit or decline
                    if args is None:
                        save_checkpoint(state)
                        return Outcome.ESCALATED
                key = f"{state.run_id}-{state.step}"  # stable across retries
                try:
                    state.observations.append(
                        call_tool(tool, args, key, max_retries, deadline))
                except PermissionError:
                    save_checkpoint(state)
                    return Outcome.ESCALATED
                except Exception as exc:  # fatal or retries exhausted
                    state.observations.append(f"tool failed: {exc}")
                    save_checkpoint(state)
                    return Outcome.GAVE_UP
        save_checkpoint(state)  # checkpoint after every step

    save_checkpoint(state)  # caller pushes this state to the dead-letter queue
    return Outcome.GAVE_UP
</code></pre>
<p><code>run_agent</code> returns an <code>Outcome</code> (SUCCEEDED, GAVE_UP or ESCALATED) and saves the state before every return, so a person or a replay job can pick up from the last step.</p>
<p>One limit to know: the loop checks the deadline between steps, so each tool function must enforce its own per-step timeout.</p>
<h3>How does this map to agent frameworks?</h3>
<p>The same loop appears in LangGraph, the OpenAI Agents SDK and Microsoft Agent Framework under different names. Your state object becomes the framework&#8217;s state or session, the checkpoint function becomes its persistence layer, and the approval callback becomes an interrupt or approval hook. Check each framework&#8217;s documentation for exact signatures. Pin versions, because the OpenAI SDK is still 0.x (0.23.1 on Oct 2, 2026) and LangGraph ships frequent patch releases (1.2.16 on Oct 16, 2026).</p>
<h2 id="secure-agentic-workflows-with-owasp-agentic-top-10-controls">Secure Agentic Workflows with OWASP Agentic Top 10 Controls</h2>
<p>Apply <strong>least agency</strong>. An agent gets only the autonomy, tools and credentials the task needs, and it earns more only after evaluation. Use the OWASP Top 10 for Agentic Applications 2026 (released Dec 9, 2025, ASI01 to ASI10) as your review checklist.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> Think of a temp worker given a key card that opens only the rooms needed today, and that stops working tonight. Unlike a human temp, an agent can be tricked by a sentence hidden in a document it reads. So the key card limits matter even more.</p>
</blockquote>
<h3>Which OWASP agentic risks should you check?</h3>
<p>Here are the ten risks in their published order, each with a typical symptom and a control.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>OWASP risk</th>
<th>What it looks like</th>
<th>Control</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Agent Goal Hijack</strong></td>
<td>A web page or email changes the agent&#8217;s objective</td>
<td>Treat retrieved content and tool output as data, never as instructions</td>
</tr>
<tr>
<td><strong>Tool Misuse</strong></td>
<td>A valid tool is called with harmful arguments</td>
<td>Scoped tools with parameter validation</td>
</tr>
<tr>
<td><strong>Identity and Privilege Abuse</strong></td>
<td>One broad key is reused across tasks</td>
<td>Short-lived, task-scoped credentials</td>
</tr>
<tr>
<td><strong>Agentic Supply Chain Vulnerabilities</strong></td>
<td>A compromised plugin or tool server</td>
<td>Pin versions, vet tool servers, record what you run</td>
</tr>
<tr>
<td><strong>Unexpected Code Execution</strong></td>
<td>Generated code runs on the host</td>
<td>Sandboxed execution</td>
</tr>
<tr>
<td><strong>Memory and Context Poisoning</strong></td>
<td>Bad facts saved to memory affect later runs</td>
<td>Validate what is stored; expire and audit memory</td>
</tr>
<tr>
<td><strong>Insecure Inter-Agent Communication</strong></td>
<td>One agent trusts another&#8217;s message blindly</td>
<td>Authenticate agents; validate messages like any input</td>
</tr>
<tr>
<td><strong>Cascading Failures</strong></td>
<td>One bad step spreads through many agents</td>
<td>Loop limits, budgets, kill switch</td>
</tr>
<tr>
<td><strong>Human-Agent Trust Exploitation</strong></td>
<td>A confident answer persuades a user to approve a bad action</td>
<td>Show proposed arguments; approval for sensitive actions</td>
</tr>
<tr>
<td><strong>Rogue Agents</strong></td>
<td>An agent drifts from its intended behaviour</td>
<td>Audit logs, monitoring, kill switch</td>
</tr>
</tbody>
</table>
</figure>
<p>In simple terms, a <strong>sandbox</strong> is a locked room where code can run without touching your real systems.</p>
<h3>How do MCP tool servers change the security picture?</h3>
<p>As of 2026-10-17, the current MCP revision is 2026-07-28 (the previous final one is 2025-11-25). In simple terms, MCP (Model Context Protocol) is a standard way for agents to discover and call tools on a server.</p>
<p>The new revision is <strong>stateless</strong>. The initialize handshake and the <code>Mcp-Session-Id</code> header are gone. Each request now carries its protocol version, client identity and capabilities in <code>_meta</code>. The revision also adds Multi Round-Trip Requests, header-based routing, cacheable list results, hardened authorization and an extensions framework.</p>
<ul>
<li><strong>Scale freely:</strong> servers can sit behind a load balancer without sticky sessions.</li>
<li><strong>Keep state out of the server:</strong> hold per-session data in your agent state, where you can checkpoint and audit it.</li>
<li><strong>Test before migrating:</strong> the C# SDK v2.0 implements this spec, but the Python, TypeScript, Go and C# SDKs are in beta.</li>
</ul>
<p><strong>Use Case:</strong> A healthcare scheduling agent gets a read-only calendar tool to find open slots. A separate booking tool sits behind human approval, which shows the patient, slot and clinician. Its credential is valid only for that task. Every call lands in the audit log.</p>
<h2 id="observe-evaluate-and-tune-agents-tracing-evals-cost-and-late">Observe, Evaluate and Tune Agents: Tracing, Evals, Cost and Latency</h2>
<p><a href="https://thecodebuzz.com/header-propagation-middleware-net-core/">Trace every run as one tree</a>, evaluate whole trajectories (the full sequence of steps, not just the final answer), and set your limits and model tiers from that data. This is the core of AI agent observability and guardrails.</p>
<h3>How do you trace an agent run?</h3>
<p>Record each run as one trace with a span per step. In simple terms, a <strong>span</strong> is a timed record of one action, and a <strong>trace</strong> is the tree of spans for one run. <strong>OpenTelemetry</strong> is an open standard for producing and exporting this data. Its GenAI conventions put an <code>invoke_agent</code> span at the top, with <code>chat</code> spans for model calls and <code>execute_tool</code> spans for tool calls beneath it.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> A trace is like a receipt that lists every task the assistant did, in order, with the time each took. Unlike a receipt, it can also show which step failed and why.</p>
</blockquote>
<p>The animated diagram below builds this tree step by step for a refund agent and sends the finished trace to the eval suite and alerts.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/agentic-workflow-best-practices-diagram-7.gif" alt="Trace tree for a refund agent: an invoke_agent root with chat and execute_tool spans, an approval pause, an outcome summary, then eval suite and alerts." width="1200" height="675" /></p>
<figcaption>Each step of a refund-agent run becomes a span under one invoke_agent root, and the finished trace feeds the eval suite and alerts.</figcaption>
</figure>
<p>Per step, record tokens, cost, latency, retry count and the final termination outcome. Prompt and response content is opt-in, because it can contain personal data. The spec is still marked Development, so attribute names can change. Keep them in one helper.</p>
<p class="code-label"><strong>C#</strong></p>
<pre><code class="language-csharp">using System.Diagnostics;

// Simplified helper. Attribute names may change while the GenAI spec is in Development.
public static class AgentTrace
{
    private static readonly ActivitySource Source = new("TheCodeBuzz.Agents");

    public static Task&lt;T&gt; AgentAsync&lt;T&gt;(string agent, Func&lt;Task&lt;T&gt;&gt; body) =&gt;
        Run($"invoke_agent {agent}", "invoke_agent", "gen_ai.agent.name", agent, body);

    public static Task&lt;T&gt; ChatAsync&lt;T&gt;(string model, Func&lt;Task&lt;T&gt;&gt; body) =&gt;
        Run($"chat {model}", "chat", "gen_ai.request.model", model, body);

    public static Task&lt;T&gt; ToolAsync&lt;T&gt;(string tool, Func&lt;Task&lt;T&gt;&gt; body) =&gt;
        Run($"execute_tool {tool}", "execute_tool", "gen_ai.tool.name", tool, body);

    public static void RecordUsage(int inputTokens, int outputTokens)
    {
        Activity.Current?.SetTag("gen_ai.usage.input_tokens", inputTokens);
        Activity.Current?.SetTag("gen_ai.usage.output_tokens", outputTokens);
    }

    private static async Task&lt;T&gt; Run&lt;T&gt;(string name, string operation,
        string key, string value, Func&lt;Task&lt;T&gt;&gt; body)
    {
        using var activity = Source.StartActivity(name);
        activity?.SetTag("gen_ai.operation.name", operation);
        activity?.SetTag(key, value);
        try
        {
            return await body();
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.SetTag("error.type", ex.GetType().FullName);
            throw;
        }
    }
}</code></pre>
<p>Nested calls become child spans automatically, because <code>Activity.Current</code> flows through async code. Register the source with <code>AddSource("TheCodeBuzz.Agents")</code> in OpenTelemetry for .NET, otherwise <code>StartActivity</code> returns null and nothing is exported.</p>
<h3>How do you evaluate an agent?</h3>
<p>Build a regression suite from real tasks and score the trajectory, not only the answer. A <strong>trajectory evaluation</strong> checks the path the agent took. A <strong>regression suite</strong> is a fixed set of tests you rerun after every change.</p>
<ul>
<li><strong>Trajectory checks:</strong> right tool, right arguments, no forbidden calls, step count and cost.</li>
<li><strong>Answer quality:</strong> score the final output separately.</li>
<li><strong>Rerun on any change:</strong> prompt, tool schema or model, because they interact.</li>
<li><strong>Adversarial cases:</strong> include prompt injection hidden in tool output.</li>
<li><strong>Two environments:</strong> keep offline evals apart from sampled production evals.</li>
</ul>
<h3>How do you control cost and latency?</h3>
<p>Pick the controls below and check each against your eval suite. Every one trades something for speed or savings.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Control</th>
<th>Benefit</th>
<th>Trade-off</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Token budget per run</strong></td>
<td>Caps worst-case spend</td>
<td>Hard tasks may stop early</td>
</tr>
<tr>
<td><strong>Model tiering</strong></td>
<td>Small model for routing and extraction, larger for hard planning</td>
<td>Routing mistakes reach the weaker model</td>
</tr>
<tr>
<td><strong>Caching</strong></td>
<td>Stable prompt prefixes and cacheable MCP list results</td>
<td>Stale data if the prefix or list changes</td>
</tr>
<tr>
<td><strong>Context compression</strong></td>
<td>Summarise old steps to shrink input</td>
<td>Detail can be lost</td>
</tr>
<tr>
<td><strong>Parallel tool calls</strong></td>
<td>Lower latency for independent steps</td>
<td>Harder error handling</td>
</tr>
<tr>
<td><strong>Streaming</strong></td>
<td>Users see partial output sooner</td>
<td>Early text may be revised</td>
</tr>
</tbody>
</table>
</figure>
<p>To tune tiers, plot cost per successful task against success rate for each tier. Choose the knee of the curve, where extra spend stops buying much accuracy.</p>
<h3>Which alerts should you set?</h3>
<p>Alert on loop-limit hits, escalation rate, cost per run and tool error rate. Review every run that hits a limit, since that shows whether the limit or the task design is wrong. These signals also belong on any production AI agents checklist.</p>
<h2 id="common-anti-patterns-in-agentic-workflows">Common Anti-Patterns in Agentic Workflows</h2>
<p>Most failures in production AI agents come from a small set of repeated design mistakes. The table below lists nine of them with the symptom you will see and the fix.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> An anti-pattern is a habit that looks sensible at first and causes trouble later, like a manager who gives a new assistant the master key, no deadline and no instructions to report back. The analogy stops at one point: a human assistant would usually push back, while a language model will keep going.</p>
</blockquote>
<h3>Which Agentic Workflow Mistakes Should You Avoid?</h3>
<p>Use this table as a review list for agentic AI workflow architecture and for any production AI agents checklist.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Anti-pattern</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Agent used where a fixed workflow would do</strong></td>
<td>Slow, costly runs with varying paths for a known task</td>
<td>Use a workflow when the steps are known (see agent workflow vs autonomous agent)</td>
</tr>
<tr>
<td><strong>No loop limit or budget</strong></td>
<td>Runs that never end; surprise bills</td>
<td>Set max steps, a deadline and a cost budget</td>
</tr>
<tr>
<td><strong>Free-text tool arguments</strong></td>
<td>Malformed calls, wrong values reach the tool</td>
<td>Use typed schemas and validate before executing</td>
</tr>
<tr>
<td><strong>Retrying non-idempotent writes</strong></td>
<td>Duplicate refunds, emails or bookings</td>
<td>Retry only idempotent calls, or send idempotency keys</td>
</tr>
<tr>
<td><strong>State only in the prompt</strong></td>
<td>A crash restarts the task; context grows without control</td>
<td>Keep an explicit state object and checkpoint each step</td>
</tr>
<tr>
<td><strong>Trusting tool output or retrieved text as instructions</strong></td>
<td>The agent follows commands hidden in a web page or document</td>
<td>Treat it as data only; add injection tests</td>
</tr>
<tr>
<td><strong>Broad, long-lived credentials</strong></td>
<td>One bug or hijack exposes every system</td>
<td>Use short-lived, task-scoped credentials</td>
</tr>
<tr>
<td><strong>Evaluating only the final answer</strong></td>
<td>Right answer, but forbidden calls or wasted steps go unseen</td>
<td>Score the whole trajectory: tools, arguments, step count, cost</td>
</tr>
<tr>
<td><strong>Floating versions on a 0.x SDK</strong></td>
<td>An upgrade silently changes behavior</td>
<td>Pin exact versions and re-run the regression suite before upgrading</td>
</tr>
<tr>
<td><strong>Many agents with unclear roles</strong></td>
<td>Handoff loops, higher cost, hard debugging</td>
<td>Split only when roles need different tools, permissions or context</td>
</tr>
</tbody>
</table>
</figure>
<p>For multi-agent orchestration patterns, human-in-the-loop AI agents and AI agent observability and guardrails, see the sections that cover each in detail.</p>
<h2 id="production-readiness-checklist-for-ai-agents">Production Readiness Checklist for AI Agents</h2>
<p>Use this production AI agents checklist before you release an agent to real users. It runs from design to operations. Every item can be verified with a test, a config value or a dashboard.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>Tip:</strong> Think of the pre-flight checklist a pilot runs before takeoff. Each line is a quick, checkable fact, not an opinion. The analogy stops in one place: an agent&#8217;s checklist must be run again whenever the prompt, a tool or the model changes.</p>
</blockquote>
<p>Work through these items in order:</p>
<ol>
<li><strong>Architecture choice:</strong> Write down why a single LLM call or fixed workflow is not enough. If you can&#8217;t, don&#8217;t build an agent.</li>
<li><strong>Outcomes:</strong> Define succeeded, gave up and escalated, plus the conditions that end a run.</li>
<li><strong>Tools:</strong> Give every tool a typed schema. Validate arguments before the tool runs.</li>
<li><strong>State:</strong> Keep an explicit state object and checkpoint after every step.</li>
<li><strong>Limits:</strong> Set step, time and cost limits from replayed eval data, not guesses.</li>
<li><strong>Retries:</strong> Retry only idempotent calls. Send exhausted tasks to a dead-letter queue with their saved state.</li>
<li><strong>Approvals:</strong> Add human-in-the-loop AI agent gates on payments, deletes and outbound messages.</li>
<li><strong>Least agency:</strong> Use short-lived, <a href="https://thecodebuzz.com/secure-secrets-azure-key-vault-examples-csharp-java-python-nodejs/">task-scoped credentials</a> and sandboxed code, mapped to each OWASP agentic risk.</li>
<li><strong>Tracing:</strong> Trace each run as one tree. Keep prompt and response capture off by default.</li>
<li><strong>Regression suite:</strong> Run trajectory evals in CI on every prompt, schema or model change, including prompt injection cases that arrive through tool output.</li>
<li><strong>Versions:</strong> Pin dependencies and record the MCP spec version you tested against.</li>
<li><strong>Operations:</strong> Set alerts for loop-limit hits, escalation rate, cost per run and tool errors. Rehearse the kill switch and the rollback plan before you need them.</li>
</ol>
<h2 id="glossary-in-plain-english">Glossary in Plain English</h2>
<p>This glossary lists the terms used in this article on agentic AI workflow architecture, grouped by topic. Each analogy comes with a note where it stops being accurate.</p>
<blockquote class="tcb-plain-english" style="border-left: 4px solid #f59e0b; background: #fffbeb; margin: 18px 0; padding: 10px 16px; font-style: normal;">
<p><strong>In plain English:</strong> Treat the analogies as a phrasebook, not a specification. They help you picture an idea, and the third column says where each picture stops matching the real thing.</p>
</blockquote>
<h3>Models and Cost Terms</h3>
<p>These terms describe the model and what it costs to run.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Term</th>
<th>What it means in plain English</th>
<th>Everyday analogy</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Agentic workflow</strong></td>
<td>A program where a language model and tools work through a task in steps. Either code or the model chooses the steps.</td>
<td>An assistant working down a to-do list. Unlike a person, the model can be confidently wrong, so it needs checks.</td>
</tr>
<tr>
<td><strong>Agent</strong></td>
<td>A setup where the model itself decides the next step and which tool to use.</td>
<td>A contractor who plans the job. Unlike a contractor, it needs a hard budget and time limit.</td>
</tr>
<tr>
<td><strong>LLM</strong></td>
<td>Large language model: software that produces text based on patterns learned in training.</td>
<td>A very well-read autocomplete. It does not look facts up unless you give them to it.</td>
</tr>
<tr>
<td><strong>Tool call</strong></td>
<td>A structured request from the model asking your code to run a function.</td>
<td>An order slip passed to a kitchen. The model writes the slip, and your code does the cooking.</td>
</tr>
<tr>
<td><strong>Token</strong></td>
<td>A small piece of text (a word or part of a word) that models read and bill by.</td>
<td>Words in a telegram, priced per piece. Tokens do not map exactly to words.</td>
</tr>
<tr>
<td><strong>Context window</strong></td>
<td>The maximum number of tokens the model can consider at once.</td>
<td>A desk: only so many papers fit. Anything off the desk is not seen at all.</td>
</tr>
<tr>
<td><strong>Temperature</strong></td>
<td>A setting for how random the output is. Low values give steadier answers.</td>
<td>A dial from strict clerk to brainstorming partner. Low does not mean correct, only repeatable.</td>
</tr>
<tr>
<td><strong>Hallucination</strong></td>
<td>A confident answer that is false or invented.</td>
<td>Someone bluffing in a meeting. The model has no intent to deceive.</td>
</tr>
<tr>
<td><strong>Model tiering</strong></td>
<td>Using a small, cheap model for easy steps and a larger one for hard steps.</td>
<td>A triage nurse and a specialist. Routing mistakes by the nurse still reach the patient.</td>
</tr>
</tbody>
</table>
</figure>
<h3>Knowledge and Retrieval Terms</h3>
<p>These terms cover how knowledge gets to the agent.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Term</th>
<th>What it means in plain English</th>
<th>Everyday analogy</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Chunk</strong></td>
<td>A small piece of a longer document, cut so it can be searched and passed to the model.</td>
<td>An index card from a book. A bad cut can separate a fact from its context.</td>
</tr>
<tr>
<td><strong>Embedding</strong></td>
<td>A list of numbers that represents the meaning of a text.</td>
<td>A GPS coordinate on a map of meaning. The real map has hundreds of dimensions, not two.</td>
</tr>
<tr>
<td><strong>Vector store</strong></td>
<td>A database that finds items whose embeddings are closest to a query.</td>
<td>A library shelved by topic instead of title. &#8220;Close&#8221; means similar, not necessarily correct.</td>
</tr>
<tr>
<td><strong>Retrieval</strong></td>
<td>Fetching the most relevant chunks for a question.</td>
<td>A librarian pulling cards. If the wrong cards come back, the answer suffers.</td>
</tr>
</tbody>
</table>
</figure>
<h3>Loop and Reliability Terms</h3>
<p>These terms describe how the loop keeps running and recovers from failure.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Term</th>
<th>What it means in plain English</th>
<th>Everyday analogy</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Agent state</strong></td>
<td>The record of the plan, results so far and remaining budget.</td>
<td>A case file. It only helps if the agent actually reads and updates it.</td>
</tr>
<tr>
<td><strong>Checkpoint</strong></td>
<td>A saved copy of the state after a step, so a crashed run can resume.</td>
<td>A video-game save. It also stores tool results, which may have changed in the outside world since.</td>
</tr>
<tr>
<td><strong>Superstep</strong></td>
<td>One round of a graph workflow (a LangGraph term) where ready steps run, then state is saved.</td>
<td>A board-game turn where everyone moves, then the board updates.</td>
</tr>
<tr>
<td><strong>Idempotency</strong></td>
<td>Repeating an action has the same effect as doing it once.</td>
<td>Pressing a lift button twice. Sending an email is not idempotent unless you add an idempotency key.</td>
</tr>
<tr>
<td><strong>Dead-letter queue</strong></td>
<td>A holding area for tasks that failed every retry, kept with their saved state.</td>
<td>The undeliverable-mail room. Someone still has to open the items.</td>
</tr>
<tr>
<td><strong>Human-in-the-loop</strong></td>
<td>A person approves or edits a risky step before it runs.</td>
<td>A countersigned cheque. Too many requests lead to rubber-stamping.</td>
</tr>
<tr>
<td><strong>Orchestration</strong></td>
<td>The control logic that sequences steps, tools, retries and limits.</td>
<td>A conductor. The conductor does not play any instrument.</td>
</tr>
<tr>
<td><strong>MCP</strong></td>
<td>Model Context Protocol: a standard way for agents to talk to tool servers.</td>
<td>A USB port. The plug fits, but you still choose what to plug in.</td>
</tr>
<tr>
<td><strong>Stateless</strong></td>
<td>The server keeps nothing between requests, so each request carries what it needs.</td>
<td>A cashier who forgets you after each sale, so you bring your own receipt.</td>
</tr>
</tbody>
</table>
</figure>
<h3>Security and Observability Terms</h3>
<p>These terms cover protection, monitoring and testing.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Term</th>
<th>What it means in plain English</th>
<th>Everyday analogy</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Prompt injection</strong></td>
<td>Hidden instructions inside text the model reads, meant to change its behaviour.</td>
<td>A forged note slipped into an assistant&#8217;s inbox saying &#8220;ignore your boss&#8221;.</td>
</tr>
<tr>
<td><strong>Least agency</strong></td>
<td>Giving an agent only the autonomy and access its task needs.</td>
<td>A hotel keycard that opens only your room.</td>
</tr>
<tr>
<td><strong>Sandbox</strong></td>
<td>An isolated area where generated code runs without touching real systems.</td>
<td>A test kitchen. A badly configured one can still leak.</td>
</tr>
<tr>
<td><strong>Span / trace</strong></td>
<td>A span is one timed step. A trace is the full tree of spans for one run.</td>
<td>Parcel tracking: each scan is a span, and the whole history is the trace.</td>
</tr>
<tr>
<td><strong>OpenTelemetry</strong></td>
<td>An open standard for emitting traces and metrics to monitoring tools.</td>
<td>A common plug shape for monitoring equipment.</td>
</tr>
<tr>
<td><strong>Trajectory evaluation</strong></td>
<td>Scoring the path an agent took (tools, arguments, steps, cost), not only its final answer.</td>
<td>Marking the working in a maths exam, not just the result.</td>
</tr>
<tr>
<td><strong>Regression suite</strong></td>
<td>A fixed set of real tasks re-run after every change to catch new failures.</td>
<td>A pre-flight checklist. Passing it does not cover cases it never listed.</td>
</tr>
</tbody>
</table>
</figure>
<h2 id="key-takeaways">Key Takeaways</h2>
<p>These are the points to keep from the article, ordered from design decisions to operations.</p>
<ul>
<li><strong>Start with the simplest design:</strong> try a single LLM call with retrieval and examples first, then a fixed workflow. Use an autonomous agent only when the steps cannot be known in advance, because agents usually cost more in latency and money.</li>
<li><strong>Workflows dominate production:</strong> in the study cited earlier, 16 of 20 case studies (80%) used predefined structured workflows for control and human oversight. If code can decide the steps, let code decide them.</li>
<li><strong>Make the loop explicit and bounded:</strong> keep an explicit state object, typed and validated tool schemas, and limits for steps, time and cost. Return a clear outcome (succeeded, gave up or escalated). Set the limits from replayed real tasks, not from a universal number.</li>
<li><strong>Design for crashes and retries:</strong> checkpoint after every step, retry only idempotent calls (or use idempotency keys for writes), and send exhausted tasks to a dead-letter queue with the saved state. Choose tooling by failure semantics before framework style.</li>
<li><strong>Apply least agency and the OWASP list:</strong> give agents short-lived, task-scoped credentials and scoped tools. Treat retrieved content and tool output as untrusted data, sandbox code execution, and gate risky actions by risk level. Keep a kill switch and audit logs.</li>
<li><strong>Trace and evaluate trajectories:</strong> record each run as one trace tree and score the tools, arguments, step count and cost, not only the final answer. Re-run the regression suite whenever the prompt, tool schema or model changes.</li>
<li><strong>Pin versions and re-check dated facts:</strong> LangGraph patch releases are frequent, the OpenAI Agents SDK is still 0.x, and the OpenTelemetry GenAI conventions are still in Development. Record the MCP spec revision you target, and re-verify the version facts dated 2026-10-17 before you rely on them.</li>
</ul>
<p>That&#8217;s all! Happy coding!</p>
<p>Did this solve your problem? Please share your questions or experience in the comments below. Bookmark this page or share it with your team, and subscribe to TheCodeBuzz for more AI engineering best practices.</p>
<h2 id="faq">FAQ</h2>
<h3>What is an agentic workflow and how is it different from an AI agent?</h3>
<p>An agentic workflow is a process where code sets the steps and a language model does the work inside them (e.g. classify, look up, draft). An AI agent is different because the model itself decides which step or tool comes next. Most production systems use workflows for this reason, since a fixed path is easier to test, limit and audit. Many real systems mix both, with a fixed outer flow and one agent loop for a single open-ended step.</p>
<h3>When should I use a fixed workflow instead of an autonomous agent?</h3>
<p>Use a fixed workflow whenever you can write down the steps before the task starts. Start with a single model call plus retrieval and examples, move to a workflow if that is not enough, and use an agent only when the path depends on what the model finds along the way. Agents usually cost more and respond more slowly, so the task value must justify that. If a wrong action is expensive, add an approval gate whichever design you pick.</p>
<h3>How do I control cost and latency in an agent loop?</h3>
<p>Set hard limits first: a maximum step count, a per-step timeout, a deadline and a token or cost budget per run. Then reduce spend with model tiering (a small model for routing and extraction, a larger one for hard planning), caching of stable prompt prefixes, summarising old steps, and parallel calls for independent tools. Summarising can drop detail, so check it against your regression suite. Tune the numbers from real traces by comparing cost per successful task with success rate for each tier.</p>
<h3>How do I add human-in-the-loop approval to an agent?</h3>
<p>Pause the loop before any risky tool call, such as a payment, a delete or an outbound email, and show the reviewer the exact proposed arguments. Save the agent state at that point so the run can resume after the person approves, edits or rejects the action. Frameworks such as LangGraph offer interrupts for this, and the same idea works in plain code with an approval callback. Gate by risk level only, because asking for approval on every step leads to reviewers clicking through without reading.</p>
<h3>What are the biggest security risks for agentic applications, and how do I reduce them?</h3>
<p>The OWASP Top 10 for Agentic Applications 2026 is the best checklist, and its risks include agent goal hijack, tool misuse, identity and privilege abuse, and memory and context poisoning. The guiding rule is least agency: give the agent only the autonomy and access it has earned. In practice, use short-lived credentials scoped to the task, validate tool parameters, run generated code in a sandbox, and treat retrieved content and tool output as untrusted data, never as instructions. Add approval for sensitive actions, audit logs and a kill switch.</p>
<h2 id="sources">Sources and further reading</h2>
<ul>
<li><a href="https://www.anthropic.com/engineering/building-effective-agents" target="_blank" rel="noopener">Building Effective AI Agents</a> (Anthropic Engineering)</li>
<li><a href="https://arxiv.org/pdf/2512.04123" target="_blank" rel="noopener">Measuring Agents in Production</a> (arXiv)</li>
<li><a href="https://wf.coreweave.com/blog/agentic-workflows-in-production-mechanisms-trade-offs-costs-and-design-patterns" target="_blank" rel="noopener">Agentic workflows in production: Mechanisms, costs and trade-offs</a> (CoreWeave)</li>
<li><a href="https://genai.owasp.org/2025/12/09/owasp-top-10-for-agentic-applications-the-benchmark-for-agentic-security-in-the-age-of-autonomous-ai/" target="_blank" rel="noopener">OWASP Top 10 for Agentic Applications &#8211; The Benchmark for Agentic Security in the Age of Autonomous AI</a> (OWASP Gen AI Security Project)</li>
<li><a href="https://auth0.com/blog/owasp-top-10-agentic-applications-lessons/" target="_blank" rel="noopener">Lessons from OWASP Top 10 for Agentic Applications</a> (Auth0)</li>
<li><a href="https://pypi.org/project/langgraph/" target="_blank" rel="noopener">langgraph · PyPI</a> (PyPI)</li>
<li><a href="https://www.jbinternational.co.uk/article/view/4680" target="_blank" rel="noopener">LangGraph 1.0 and 1.2 in 2026: Why Durable State Has Changed Everything for Python Agent Developers</a> (JBI Training)</li>
<li><a href="https://github.com/openai/openai-agents-python/releases" target="_blank" rel="noopener">Releases · openai/openai-agents-python</a> (GitHub (OpenAI))</li>
<li><a href="https://developers.openai.com/api/docs/changelog" target="_blank" rel="noopener">Changelog</a> (OpenAI)</li>
<li><a href="https://visualstudiomagazine.com/articles/2026/04/06/microsoft-ships-production-ready-agent-framework-1-0-for-net-and-python.aspx" target="_blank" rel="noopener">Microsoft Ships Production-Ready Agent Framework 1.0 for .NET and Python</a> (Visual Studio Magazine)</li>
</ul>
<p>&nbsp;</p>


<p class="wp-block-paragraph">Please share this article with your friends and <a rel="noreferrer noopener" href="https://www.thecodebuzz.com/subscription/" target="_blank"><em><strong>Subscribe</strong> </em></a>to the blog to get a notification on freshly published best practices of software development.</p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">        </p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"></p><p>The post <a href="https://thecodebuzz.com/agentic-workflow-best-practices/">Agentic Workflow Best Practices: Architecture and Checklist</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/agentic-workflow-best-practices/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
