<?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>Sun, 18 Oct 2026 17:50: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>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 fetchpriority="high" 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 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 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>
		<item>
		<title>SQL Server Best Practices and Guidelines-2026</title>
		<link>https://thecodebuzz.com/sql-server-best-practices-guidelines/</link>
					<comments>https://thecodebuzz.com/sql-server-best-practices-guidelines/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 21:38:05 +0000</pubDate>
				<category><![CDATA[Database Best Practices]]></category>
		<category><![CDATA[DBA]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[SQL Server 2025]]></category>
		<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30819</guid>

					<description><![CDATA[<p>SQL Server best practices for 2022 and 2025: memory, MAXDOP, tempdb, security, backups and upgrades, with runnable T-SQL and C# examples.</p>
<p>The post <a href="https://thecodebuzz.com/sql-server-best-practices-guidelines/">SQL Server Best Practices and Guidelines-2026</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>This guide is a bookmarkable reference for developers and DBAs who run SQL Server in production. It is current for SQL Server 2022 and 2025, with runnable T-SQL, C# and Python examples. Many older checklists still repeat the &#8220;leave 4-6 GB or 10-15% for the OS&#8221; memory rule and give contradictory MAXDOP advice. They also skip the 2025 features, the current driver defaults and the upgrade pitfalls. These SQL Server best practices fix that.</p>
<p>It is different from the existing post, <em>SQL Server Best Practices for Developers and DBAs (2026 Guide)</em>. This one is organised by production concern: configuration, indexing and queries, transactions, drivers, security, backups, monitoring and upgrades. Each rule explains the why, states the trade-off and gives a command to verify the result. The guide ends with an anti-patterns table and a production readiness checklist.</p>
<p>Before you start, you need sysadmin access on a test instance and SSMS or sqlcmd. The .NET examples use <a href="https://thecodebuzz.com/connect-sql-server-database-insert-update-delete-sqlclient-csharp/">Microsoft.Data.SqlClient</a> and the Python examples use pyodbc. Support dates, build numbers and default behaviours are correct as of October 2026. Always test every change on a non-production copy first.</p>
<blockquote><p><strong>Version note:</strong> This guide targets SQL Server 2022 (latest CU27, 16.0.4295.3) and SQL Server 2025 (latest CU9, 17.0.5005.3), with Microsoft.Data.SqlClient v7.0.1.</p>
<p>&nbsp;</p></blockquote>
<p><strong>In this guide</strong></p>
<ul>
<li><a href="#sql-server-best-practices-at-a-glance">SQL Server Best Practices at a Glance</a></li>
<li><a href="#sql-server-configuration-best-practices-memory-maxdop-cost-t">SQL Server Configuration Best Practices: Memory, MAXDOP, Cost Threshold and tempdb</a></li>
<li><a href="#data-modeling-and-indexing-best-practices">Data Modeling and Indexing Best Practices</a></li>
<li><a href="#query-optimization-and-sql-server-performance-tuning-best-pr">Query Optimization and SQL Server Performance Tuning Best Practices</a></li>
<li><a href="#transactions-isolation-levels-and-optimized-locking">Transactions, Isolation Levels and Optimized Locking</a></li>
<li><a href="#connection-management-and-microsoft-data-sqlclient-best-prac">Connection Management and Microsoft.Data.SqlClient Best Practices</a></li>
<li><a href="#sql-server-security-best-practices-least-privilege-injection">SQL Server Security Best Practices: Least Privilege, Injection and Encryption</a></li>
<li><a href="#sql-server-backup-best-practices-high-availability-and-resto">SQL Server Backup Best Practices, High Availability and Restore Testing</a></li>
<li><a href="#monitoring-sql-server-wait-stats-extended-events-and-perform">Monitoring SQL Server: Wait Stats, Extended Events and Performance Counters</a></li>
<li><a href="#patching-and-upgrading-to-sql-server-2025-what-to-check-firs">Patching and Upgrading to SQL Server 2025: What to Check First</a></li>
<li><a href="#common-anti-patterns-and-sql-server-dba-production-readiness">Common Anti-Patterns and SQL Server DBA Production Readiness Checklist</a></li>
<li><a href="#key-takeaways">Key takeaways</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<p>&nbsp;</p>
<h2 id="sql-server-best-practices-at-a-glance">SQL Server Best Practices at a Glance</h2>
<p>&nbsp;</p>
<p>This guide covers production best practices for SQL Server 2022 and SQL Server 2025, from instance configuration and indexing to security, backups, monitoring and upgrades.</p>
<p>&nbsp;</p>
<p>The table below is a quick reference for developers, architects and DBAs. Each rule points to the section that explains the reason, so you can jump to the detail when you need it.</p>
<p>&nbsp;</p>
<h3>Quick Reference Table</h3>
<p>&nbsp;</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>Instance memory</td>
<td>Cap <code>max server memory</code> at about 75% of available memory (see Configuration).</td>
<td>Leaving the default of about 2 TB.</td>
</tr>
<tr>
<td>MAXDOP and cost threshold</td>
<td>Start from the NUMA-based MAXDOP rule and tune cost threshold by measurement (see Configuration).</td>
<td>MAXDOP 1 server-wide, or cost threshold left at 5.</td>
</tr>
<tr>
<td>tempdb</td>
<td>Use equally sized, pre-sized data files with fixed growth (see Configuration).</td>
<td>One small file that relies on autogrow.</td>
</tr>
<tr>
<td>Storage layout</td>
<td>Separate data, log and tempdb volumes; enable instant file initialization (see Configuration).</td>
<td>Putting everything on one shared volume.</td>
</tr>
<tr>
<td>Data types and keys</td>
<td>Use the narrowest correct type and a narrow, unique, ever-increasing clustered key (see Data Modeling).</td>
<td>Random GUID clustered keys and <code>nvarchar(max)</code> for short values.</td>
</tr>
<tr>
<td>Indexing</td>
<td>Index for WHERE, JOIN and ORDER BY, and use INCLUDE columns (see Data Modeling).</td>
<td>Duplicate and unused indexes.</td>
</tr>
<tr>
<td>Query patterns</td>
<td>Write SARGable predicates and set-based code (see Query Optimization).</td>
<td><code>SELECT *</code> and functions on indexed columns.</td>
</tr>
<tr>
<td>Isolation level</td>
<td>Use RCSI for read-heavy OLTP after testing (see Transactions).</td>
<td>Long transactions and untested isolation changes.</td>
</tr>
<tr>
<td>Connection strings</td>
<td>Keep <code>Encrypt=Mandatory</code> or use Strict (see Connection Management).</td>
<td><code>TrustServerCertificate=true</code> in production.</td>
</tr>
<tr>
<td>Authentication and permissions</td>
<td>Use Windows or Entra auth and a custom role (see Security).</td>
<td>Using <code>sa</code> or <code>db_owner</code> for apps.</td>
</tr>
<tr>
<td>Dynamic SQL</td>
<td>Use <code>sp_executesql</code> with typed parameters (see Security).</td>
<td>Concatenating user input into SQL strings.</td>
</tr>
<tr>
<td>Backups</td>
<td>Use <code>CHECKSUM</code>, compression and off-server copies (see Backups).</td>
<td>FULL recovery with no log backups.</td>
</tr>
<tr>
<td>Restore testing</td>
<td>Restore on a schedule and run <code>DBCC CHECKDB</code> (see Backups).</td>
<td>Trusting <code>RESTORE VERIFYONLY</code> alone.</td>
</tr>
<tr>
<td>Patching</td>
<td>Patch on a cadence and test on a copy first (see Patching and Upgrading).</td>
<td>Switching between CU and GDR tracks casually.</td>
</tr>
<tr>
<td>Monitoring</td>
<td>Start with wait stats, then Query Store and Extended Events (see Monitoring).</td>
<td>Alerting only on whether a job ran.</td>
</tr>
</tbody>
</table>
</figure>
<h3></h3>
<h3>How to Read This Guide</h3>
<p>&nbsp;</p>
<p>Treat every rule as a default, not a law. Where a rule does not apply, the section states the trade-off and the case where you should do something different.</p>
<p>&nbsp;</p>
<p>Test each change on a non-production copy and measure before and after.</p>
<p>&nbsp;</p>
<h2 id="sql-server-configuration-best-practices-memory-maxdop-cost-t">SQL Server Configuration Best Practices: Memory, MAXDOP, Cost Threshold and tempdb</h2>
<p>&nbsp;</p>
<p>These instance-level settings decide how SQL Server uses memory, CPU and temporary storage. Most of them ship with defaults that suit a demo machine, not a production server, so review them on every new install, on 2022 and on 2025.</p>
<p>&nbsp;</p>
<h3>SQL Server Max Server Memory Setting</h3>
<p>&nbsp;</p>
<p>Cap max server memory on every version. The default is about 2 TB, which means SQL Server will take whatever it can and leave the OS and other processes to fight for the rest.</p>
<p>Current Microsoft docs suggest a starting point of about 75% of the available memory not used by other processes, including other instances on the same host. Leave min server memory at 0. Setup on Windows has suggested a value since SQL Server 2019, so check what it picked.</p>
<p>&nbsp;</p>
<p>The older advice was to leave 4-6 GB, or 10-15%, for the OS. A percentage of what is actually available is a better baseline because it already subtracts a second instance, an SSIS service, antivirus or a monitoring agent. A fixed reserve ignores all of them.</p>
<p>&nbsp;</p>
<p>If Lock Pages in Memory (LPIM) is granted to the service account, set a specific value. LPIM stops Windows from trimming SQL Server&#8217;s memory, so a wrong cap can starve the OS.</p>
<p>&nbsp;</p>
<h3>MAXDOP: Why OLTP Does Not Automatically Mean MAXDOP</h3>
<p>&nbsp;</p>
<p>MAXDOP (maximum degree of parallelism) is the number of CPUs one query can use. Some teams say &#8220;OLTP means MAXDOP 1&#8221;, while Microsoft publishes a NUMA-based rule. They conflict because the first is a workaround for parallelism waits and the second is a general default.</p>
<p>&nbsp;</p>
<p>Start from the Microsoft guidance. A NUMA node (Non-Uniform Memory Access node) is a group of processors that share local memory. Keeping a query inside one node avoids slower cross-node memory access. The table below shows the starting values.</p>
<p>&nbsp;</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Server layout</th>
<th>Recommended MAXDOP</th>
</tr>
</thead>
<tbody>
<tr>
<td>One NUMA node, 8 or fewer logical processors</td>
<td>Up to the number of logical processors</td>
</tr>
<tr>
<td>One NUMA node, more than 8 logical processors</td>
<td>8</td>
</tr>
<tr>
<td>Several NUMA nodes, 16 or fewer logical processors per node</td>
<td>Up to the number of logical processors in one node</td>
</tr>
<tr>
<td>Several NUMA nodes, more than 16 logical processors per node</td>
<td>Half the logical processors in one node, with a maximum of 16</td>
</tr>
</tbody>
</table>
</figure>
<p>&nbsp;</p>
<p>For example, a server with two NUMA nodes of 24 logical processors each would start at 12. A server with two nodes of 40 logical processors each would start at 16, because half of 40 is 20 and the cap is 16.</p>
<p>&nbsp;</p>
<ul>
<li><strong>Mixed workloads:</strong> Use <code>ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP</code> so each database gets its own limit.</li>
<li><strong>One workload that needs a different limit:</strong> Create a Resource Governor workload group with its own <code>MAX_DOP</code>.</li>
<li><strong>MAXDOP 1 server-wide:</strong> Avoid it. Parallel index builds and reporting queries lose parallelism, and maintenance windows get longer.</li>
</ul>
<h3></h3>
<h3>Cost Threshold for Parallelism and Optimize for Ad Hoc Workloads</h3>
<p>&nbsp;</p>
<p>The default cost threshold for parallelism is 5, which is too low for modern hardware. Small queries go parallel and waste threads on coordination.</p>
<p>&nbsp;</p>
<p>Raise it based on observed plan costs. A value in the 25 to 50 range is a reasonable test value, not a rule. After the change, compare CXPACKET and CXCONSUMER waits before and after, and check that your important queries did not slow down.</p>
<p>&nbsp;</p>
<p>Enable <code>optimize for ad hoc workloads</code> when the plan cache fills with single-use plans, e.g. an ORM that sends unparameterized text. On first run SQL Server stores only a small plan stub, and the full plan is cached on the second run. The trade-off is one extra compile for queries that do repeat.</p>
<p>&nbsp;</p>
<h3>tempdb Configuration</h3>
<p>&nbsp;</p>
<p>Use several data files of equal size, pre-sized, with a fixed growth increment, on fast storage. Autogrow should be an emergency only, not the sizing method.</p>
<p>Many sessions create temporary objects at once, and they all compete for the allocation pages (PFS, GAM, SGAM) in a single file. That shows up as PAGELATCH waits. More files spread the allocations. SQL Server fills files in proportion to free space, so unequal sizes send most of the work to one file.</p>
<p>Use this rule for the file count, which follows Microsoft guidance:</p>
<ol>
<li>If the server has 8 or fewer logical processors, create one data file per logical processor (e.g. 4 logical processors, 4 files).</li>
<li>If the server has more than 8 logical processors, start with 8 data files.</li>
<li>If PAGELATCH contention on tempdb allocation pages continues with 8 files, add files in multiples of 4, and stop when the contention goes away.</li>
</ol>
<p>More files than needed do not help and add some overhead, so do not match the core count on a large server.</p>
<h3>SQL Server 2025 Additions</h3>
<ul>
<li><strong>tempdb space governance:</strong> Resource Governor can limit tempdb space per workload group. A workload that exceeds its limit is stopped with an error.</li>
<li><strong>Accelerated Database Recovery in tempdb:</strong> Now supported.</li>
<li><strong>Resource Governor in Standard edition:</strong> It has the same functionality as Enterprise. Standard 2025 is limited to the lesser of 4 sockets or 32 cores, and 256 GB of buffer pool per instance.</li>
<li></li>
</ul>
<h3>Storage and OS Settings</h3>
<p>&nbsp;</p>
<p>Separate data, log and tempdb volumes where your storage design allows it. Put the log on fast storage or RAID 10, because log writes are sequential and every commit waits for them (WRITELOG).</p>
<p>Enable instant file initialization (IFI) by granting the &#8220;Perform volume maintenance tasks&#8221; privilege to the SQL Server service account. With IFI, data file growth and restores skip zeroing the new space, so they finish much faster.</p>
<p>Log files are different, and the behavior depends on the version. In SQL Server 2019 and earlier, log files are always zero-initialized. In SQL Server 2022 and later, transaction log growth events of up to 64 MB can also use IFI. Larger log growths, and the initial creation of a log file, are still zeroed.</p>
<p>For the versions this guide targets, set the log autogrowth increment to 64 MB or less so that emergency growths are fast. Still pre-size the log to its working size, because a log that grows often creates many virtual log files and slows recovery.</p>
<p>&nbsp;</p>
<h3>Sample Configuration Script</h3>
<p>The example below assumes one instance on a 32 GB server, so 75% is 24576 MB. It also assumes a single NUMA node with 16 logical processors. By the Microsoft rule, a single node with more than 8 logical processors gets MAXDOP 8, and the same layout gives 8 tempdb data files. Adjust all values for your own hardware.</p>
<p>&nbsp;</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;

EXEC sp_configure 'max server memory (MB)', 24576;
EXEC sp_configure 'cost threshold for parallelism', 50;
EXEC sp_configure 'optimize for ad hoc workloads', 1;
RECONFIGURE;

-- Run in the user database that needs its own limit.
-- 8 = single NUMA node with 16 logical processors (more than 8, so cap at 8).
ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP = 8;

-- Verify the instance settings
SELECT name, value, value_in_use
FROM sys.configurations
WHERE name IN ('max server memory (MB)',
               'min server memory (MB)',
               'cost threshold for parallelism',
               'optimize for ad hoc workloads');

-- Check the NUMA layout and logical processor count used for MAXDOP and tempdb sizing
SELECT cpu_count FROM sys.dm_os_sys_info;

SELECT node_id, online_scheduler_count
FROM sys.dm_os_nodes
WHERE node_state_desc = N'ONLINE';

-- Check tempdb file layout (expect 8 data files on this 16-processor server)
SELECT name, physical_name,
       size * 8 / 1024 AS size_mb,
       growth, is_percent_growth
FROM sys.master_files
WHERE database_id = 2;</code></pre>
<p>Run the NUMA queries first and change MAXDOP if your layout differs. The last query should show equal <code>size_mb</code> values, a fixed growth in MB and <code>is_percent_growth</code> set to 0 for every data file.</p>
<h2 id="data-modeling-and-indexing-best-practices">Data Modeling and Indexing Best Practices</h2>
<p>Good SQL Server performance starts with the table design, not the server settings. Model the data cleanly, choose small data types, and add only the indexes your queries use. These rules apply to both SQL Server 2022 and 2025.</p>
<h3>Model to Third Normal Form First</h3>
<p>Start with third normal form (3NF), where each non-key column depends only on the key. Denormalize only when a measured query problem justifies it.</p>
<p>Normalized tables avoid update anomalies, because a customer address lives in one row and not in every order. Rows stay smaller, so more rows fit on a page. Updates touch fewer rows, which means fewer locks.</p>
<h3>Use the Narrowest Correct Data Type and Add Constraints</h3>
<p>Pick the smallest type that safely holds the data. The clustered index key is carried into every nonclustered index as the row locator, so a wide clustered key makes every nonclustered index larger. Wide columns in any index key also add to index size, buffer pool memory and I/O.</p>
<ul>
<li><strong>int vs bigint:</strong> Use <code>int</code> unless you expect more than about 2.1 billion rows.</li>
<li><strong>date vs datetime2:</strong> Use <code>date</code> when no time is needed, and <code>datetime2(0)</code> for second precision.</li>
<li><strong>Strings:</strong> Avoid <code>nvarchar(max)</code> for short values. Use a sized type such as <code>nvarchar(100)</code>.</li>
</ul>
<p>Declare primary key, foreign key, unique and check constraints. The optimizer uses them to build better plans, and they reject bad data at the source, before it reaches the application.</p>
<h3>Choose a Narrow, Ever-Increasing Clustered Key</h3>
<p>Give every table a clustered index on a narrow, unique, ever-increasing key, such as an <code>int IDENTITY</code> column. New rows are appended at the end of the table, and the small key is copied into each nonclustered index.</p>
<p>A random GUID from <code>NEWID()</code> inserts rows at random positions, which causes page splits and fragmentation. A GUID is also 16 bytes, which makes it a wide clustered key. If you need GUIDs, use <code>NEWSEQUENTIALID()</code> as a column default, or keep an identity column as the clustered key and the GUID as a separate unique column.</p>
<h3>Design Nonclustered Indexes for Your Queries</h3>
<p>Index the columns used in <code>WHERE</code>, <code>JOIN</code> and <code>ORDER BY</code>. Put equality columns first and the range column after them. Use <code>INCLUDE</code> for columns that are only selected, so the index covers the query without key lookups.</p>
<p>Example: a retail site lists one customer&#8217;s recent orders, filtering on <code>CustomerId</code> (equality) and <code>OrderDate</code> (range).</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">CREATE TABLE dbo.Orders
(
    OrderId     int IDENTITY(1,1) NOT NULL,
    CustomerId  int NOT NULL,
    OrderDate   datetime2(0) NOT NULL,
    Status      tinyint NOT NULL,
    TotalAmount decimal(10,2) NOT NULL,
    CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderId),
    CONSTRAINT FK_Orders_Customers FOREIGN KEY (CustomerId)
        REFERENCES dbo.Customers (CustomerId),
    CONSTRAINT CK_Orders_TotalAmount CHECK (TotalAmount &gt;= 0)
);

CREATE NONCLUSTERED INDEX IX_Orders_Customer_Date
ON dbo.Orders (CustomerId, OrderDate)
INCLUDE (Status, TotalAmount);</code></pre>
<p>This creates a narrow clustered key, a foreign key, a check constraint and a covering index for the customer-orders query.</p>
<h3>Remove Duplicate and Unused Indexes</h3>
<p>Every index slows inserts, updates and deletes and uses storage. Check <code>sys.dm_db_index_usage_stats</code> to find indexes that are written to but never read.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">SELECT OBJECT_SCHEMA_NAME(i.object_id) AS SchemaName,
       OBJECT_NAME(i.object_id)        AS TableName,
       i.name                          AS IndexName,
       ISNULL(s.user_seeks, 0)   AS UserSeeks,
       ISNULL(s.user_scans, 0)   AS UserScans,
       ISNULL(s.user_lookups, 0) AS UserLookups,
       ISNULL(s.user_updates, 0) AS UserUpdates
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS s
       ON s.object_id = i.object_id
      AND s.index_id = i.index_id
      AND s.database_id = DB_ID()
WHERE i.type_desc = 'NONCLUSTERED'
  AND i.is_primary_key = 0
  AND i.is_unique_constraint = 0
  AND OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
  AND ISNULL(s.user_seeks, 0) + ISNULL(s.user_scans, 0) + ISNULL(s.user_lookups, 0) = 0
ORDER BY UserUpdates DESC;</code></pre>
<p>This lists nonclustered indexes with no reads, sorted by write cost. The usage counters reset when the instance restarts, so observe a full business cycle (e.g. a month-end close) before dropping anything.</p>
<h3>Maintain Statistics First, Fragmentation Second</h3>
<p>Update statistics regularly, because the optimizer chooses plans from them. Stale statistics cause bad row estimates and bad plans.</p>
<p>For fragmentation, act on a threshold and not on a blanket schedule. Measure it with <code>sys.dm_db_index_physical_stats</code>, then pick an action from the table below. These numbers are starting points only, so adjust them after you see how your own workload and storage respond.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Index fragmentation</th>
<th>Starting-point action</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>Below 5%</td>
<td>Do nothing</td>
<td>The maintenance cost is higher than any benefit.</td>
</tr>
<tr>
<td>5% to 30%</td>
<td><code>ALTER INDEX ... REORGANIZE</code></td>
<td>Reorganize is an online operation that compacts pages in place, using little extra space.</td>
</tr>
<tr>
<td>Above 30%</td>
<td><code>ALTER INDEX ... REBUILD</code></td>
<td>Rebuild creates the index again, which removes fragmentation and updates the index statistics with a full scan.</td>
</tr>
</tbody>
</table>
</figure>
<p>Ignore small indexes, for example those under about 1,000 pages, because their fragmentation figures are not meaningful and rarely affect performance. Fragmentation matters less on SSD storage, but statistics matter just as much.</p>
<h3>SQL Server 2025 Vector Data</h3>
<p>The <code>vector</code> data type is generally available in SQL Server 2025. DiskANN vector indexes are still preview only, need the <code>PREVIEW_FEATURES</code> database scoped configuration, and are not recommended for production yet.</p>
<h2 id="query-optimization-and-sql-server-performance-tuning-best-pr">Query Optimization and SQL Server Performance Tuning Best Practices</h2>
<p>Most SQL Server performance tuning comes down to a few habits: write predicates the optimizer can seek on, return only the data you need, reuse plans, and watch for plan regressions with Query Store.</p>
<h3>Write SARGable Predicates</h3>
<p>A SARGable predicate (Search ARGument able) lets the engine use an index seek. Wrapping an indexed column in a function forces a scan, because the function must run on every row.</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">-- Bad: function on the column, index on OrderDate cannot be used for a seek
SELECT OrderId, TotalAmount
FROM dbo.Orders
WHERE YEAR(OrderDate) = 2026;

-- Good: range on the bare column, index seek is possible
SELECT OrderId, TotalAmount
FROM dbo.Orders
WHERE OrderDate &gt;= '20260101'
  AND OrderDate &lt;  '20270101';</code></pre>
<p>The rewritten query returns the same rows and reads only the matching index range.</p>
<h3>Return Less Data, Use Fewer Round Trips</h3>
<ul>
<li><strong>Select only needed columns:</strong> Avoid <code>SELECT *</code>. It reads and sends extra data, and it stops narrow covering indexes from being used.</li>
<li><strong>SET NOCOUNT ON:</strong> Add it at the top of procedures to stop the &#8220;rows affected&#8221; message after every statement.</li>
<li><strong>Set-based code:</strong> Use one <code>UPDATE ... JOIN</code> instead of a cursor. A cursor processes one row at a time, which costs more CPU and more statements.</li>
</ul>
<p>Example: a retail nightly job that updates stock for 200,000 products should run one set-based statement, not a loop that fires 200,000 updates.</p>
<h3>Handle Parameter Sniffing</h3>
<p>Parameterized queries and stored procedures let SQL Server reuse plans. Parameter sniffing means the plan is built for the first parameter value it sees, which can be a poor fit for later values.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Option</th>
<th>Use when</th>
<th>Trade-off</th>
</tr>
</thead>
<tbody>
<tr>
<td>Parameter-sensitive plan (PSP) optimization (SQL Server 2022 and later, compatibility level 160 or higher)</td>
<td>A parameterized query runs against skewed data, so one value returns a few rows and another returns millions</td>
<td>Applies only to some predicate shapes, and the engine decides when to use it. It is not a fix for every sniffing problem, so check the plans in Query Store</td>
</tr>
<tr>
<td><code>OPTION (RECOMPILE)</code></td>
<td>Rare, expensive queries with very uneven data</td>
<td>Compile cost on every run</td>
</tr>
<tr>
<td><code>OPTIMIZE FOR</code></td>
<td>One typical value is known</td>
<td>Other values may run slowly</td>
</tr>
<tr>
<td>Query Store plan forcing</td>
<td>A known good plan exists</td>
<td>Forced plan can go stale as data changes</td>
</tr>
</tbody>
</table>
</figure>
<p>On 2022 and later, PSP optimization is the first thing to try when you are on compatibility level 160. It lets one parameterized statement keep more than one plan for different parameter ranges. If it does not cover your query, move down the table.</p>
<h3>Use Query Store</h3>
<p>Query Store records queries, plans and runtime statistics inside the database. From SQL Server 2022, it is ON by default for new databases. The &#8220;enable it&#8221; step therefore mostly applies to databases that were upgraded or restored from an older instance, because they keep their earlier setting.</p>
<p>Check which databases still have it off, and turn it on for the ones you care about. Use <code>QUERY_CAPTURE_MODE = AUTO</code> so trivial queries do not fill the store.</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">SELECT name, is_query_store_on
FROM sys.databases
WHERE database_id &gt; 4;</code></pre>
<p>This lists user databases and shows whether Query Store is on for each one.</p>
<p>On readable secondary replicas, the defaults differ by version. In SQL Server 2025, Query Store on readable secondaries is on by default. In SQL Server 2022 it was off and needed trace flag 12606.</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">ALTER DATABASE SalesDb SET QUERY_STORE = ON
(
    OPERATION_MODE = READ_WRITE,
    QUERY_CAPTURE_MODE = AUTO,
    MAX_STORAGE_SIZE_MB = 1024,
    INTERVAL_LENGTH_MINUTES = 60,
    SIZE_BASED_CLEANUP_MODE = AUTO
);</code></pre>
<p>The sizes above are example values. Adjust them to your workload.</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">SELECT TOP (10)
    q.query_id,
    LEFT(qt.query_sql_text, 200) AS query_text,
    SUM(rs.count_executions * rs.avg_duration) / 1000.0 AS total_duration_ms
FROM sys.query_store_query AS q
JOIN sys.query_store_query_text AS qt ON qt.query_text_id = q.query_text_id
JOIN sys.query_store_plan AS p ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id
GROUP BY q.query_id, qt.query_sql_text
ORDER BY total_duration_ms DESC;</code></pre>
<p>This lists the ten queries that consumed the most total time. Durations are stored in microseconds, so the query divides by 1000.</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 17;

-- Remove the forced plan when it is no longer needed
EXEC sys.sp_query_store_unforce_plan @query_id = 42, @plan_id = 17;</code></pre>
<p>Replace the IDs with values from the previous query and the Query Store reports.</p>
<h3>Test Feedback Features and Compatibility Levels</h3>
<p>The optimizer can adjust some settings for a query after it has run, using what it saw on earlier executions. Two feedback features matter most, and each needs a minimum compatibility level.</p>
<ul>
<li><strong>Memory grant feedback:</strong> Resizes the memory granted to a query when earlier runs got too much or too little, which reduces tempdb spills and wasted memory. Batch mode needs compatibility level 140 or higher. Row mode needs 150 or higher. In SQL Server 2022, the percentile and persisted versions of this feedback need Query Store to be on and in read-write mode.</li>
<li><strong>DOP feedback:</strong> Lowers the degree of parallelism (DOP, the number of threads a query uses) for parallel queries that do not benefit from it. It is available in SQL Server 2022 and later, needs compatibility level 160 or higher, and needs Query Store in read-write mode. In 2022, check that the <code>DOP_FEEDBACK</code> database scoped configuration is on for the database.</li>
</ul>
<p>Raise the compatibility level on a copy of the database first, compare plans, and force the old plan where a regression appears. Check the Microsoft documentation for the exact compatibility level that matches your SQL Server version before you change it.</p>
<h3>Read Execution Plans</h3>
<ol>
<li><strong>Estimated vs actual rows:</strong> Large gaps usually mean stale statistics or a poor cardinality estimate.</li>
<li><strong>Spills to tempdb:</strong> A sort or hash warning means the memory grant was too small.</li>
<li><strong>Key lookups:</strong> Many lookups suggest the index needs an <code>INCLUDE</code> column.</li>
<li><strong>Implicit conversions:</strong> A parameter type that differs from the column type (e.g. <code>nvarchar</code> against <code>varchar</code>) can block an index seek.</li>
</ol>
<h2 id="transactions-isolation-levels-and-optimized-locking">Transactions, Isolation Levels and Optimized Locking</h2>
<p>Keep transactions short, touch tables in the same order everywhere, and use row versioning (Read Committed Snapshot Isolation, or RCSI) for read-heavy OLTP so readers don&#8217;t block writers. These habits are part of SQL Server performance tuning best practices because blocking often looks like a slow query.</p>
<h3>Isolation Levels Compared</h3>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Level</th>
<th>What it prevents</th>
<th>Version store cost</th>
<th>Use when</th>
</tr>
</thead>
<tbody>
<tr>
<td>READ COMMITTED (locking)</td>
<td>Dirty reads</td>
<td>None</td>
<td>Default; low-concurrency workloads</td>
</tr>
<tr>
<td>READ COMMITTED SNAPSHOT (RCSI)</td>
<td>Dirty reads; readers don&#8217;t wait for writers</td>
<td>Row versions in tempdb, or in the PVS with ADR</td>
<td>Read-heavy OLTP</td>
</tr>
<tr>
<td>SNAPSHOT</td>
<td>Dirty reads, non-repeatable reads, phantoms within a transaction</td>
<td>Same store; versions live as long as the oldest open transaction</td>
<td>Consistent multi-statement reads</td>
</tr>
<tr>
<td>SERIALIZABLE</td>
<td>All of the above, using range locks</td>
<td>None, but heavy locking</td>
<td>Strict invariants, used rarely</td>
</tr>
</tbody>
</table>
</figure>
<h3>RCSI Trade-offs</h3>
<p>RCSI makes a SELECT read the last committed version of a row instead of waiting for a lock. The cost is extra version storage, normally in tempdb. With Accelerated Database Recovery (ADR), the version store lives in the user database (the persistent version store, PVS).</p>
<p>Behaviour also changes. Code that relied on a blocking read to wait for a writer, such as a &#8220;read then update&#8221; pattern, now sees the old value. Test this in a copy of the database first.</p>
<h3>Optimized Locking in SQL Server 2025</h3>
<p>SQL Server 2025 adds optimized locking, which uses transaction ID (TID) locking and lock after qualification (LAQ). A TID lock replaces many row locks, and LAQ takes locks only on the rows that match the predicate. This reduces blocking and lock memory use, avoids lock escalation, and needs no application code change. It is a good reason to test 2025 with write-heavy workloads.</p>
<p>Optimized locking is not on by default for every database. It requires Accelerated Database Recovery (ADR) to be enabled on the database, so turn on ADR first and check the result in <code>sys.databases</code>. Confirm the exact database-level setting for optimized locking in the Microsoft documentation for your build.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">-- Prerequisite for optimized locking: ADR on the database.
-- Test first and run in a quiet window.
ALTER DATABASE BankDb SET ACCELERATED_DATABASE_RECOVERY = ON;
GO

SELECT name, is_accelerated_database_recovery_on
FROM sys.databases
WHERE name = N'BankDb';</code></pre>
<p>This enables ADR and confirms it with a check of <code>is_accelerated_database_recovery_on</code>.</p>
<p>RCSI and optimized locking address different parts of the blocking problem, so measure them with and without RCSI on your own workload instead of assuming the combination is better. The animated diagram below compares classic locking with optimized locking plus RCSI for two sessions on the same table.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/sql-server-best-practices-guidelines-diagram-5.gif" alt="Classic locking escalates to a table lock and blocks a SELECT; optimized locking with RCSI uses one TID lock and does not block." width="1200" height="675" /><figcaption>Classic locking escalates and blocks readers; optimized locking with RCSI uses a TID lock and readers see row versions.</figcaption></figure>
<h3>Deadlocks and Retries</h3>
<p>When SQL Server picks your session as a deadlock victim, it returns error 1205. Retry in application code with a bounded count (e.g. three attempts) and a short backoff. To see the cause, read the deadlock graph from the <code>system_health</code> Extended Events session.</p>
<h3>Finance Transfer Example</h3>
<p>Move money in one short transaction. Validate the inputs first, then update the two accounts with two separate statements, lowest ID first. Because every transfer takes the locks in the same order, two opposite transfers (A to B and B to A) can&#8217;t deadlock on these two rows.</p>
<p>The balance check sits inside the debit <code>UPDATE</code>&#8216;s <code>WHERE</code> clause. The row is locked when the check runs, so it sees the current balance. A separate <code>SELECT</code> before the update could read an older row version under RCSI.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">-- Enable RCSI (rolls back open transactions, so run in a quiet window)
ALTER DATABASE BankDb SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
GO

CREATE OR ALTER PROCEDURE dbo.TransferFunds
    @FromId int, @ToId int, @Amount decimal(19,4)
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    -- Validate inputs before opening a transaction
    IF @Amount IS NULL OR @Amount &lt;= 0
        THROW 50001, 'Amount must be greater than zero.', 1;
    IF @FromId IS NULL OR @ToId IS NULL OR @FromId = @ToId
        THROW 50002, 'Source and target accounts must be different.', 1;

    -- Work out the lock order: lowest AccountId first
    DECLARE @LowId  int = CASE WHEN @FromId &lt; @ToId THEN @FromId ELSE @ToId END;
    DECLARE @HighId int = CASE WHEN @FromId &lt; @ToId THEN @ToId ELSE @FromId END;

    -- Signed change for each account: negative for the debit, positive for the credit
    DECLARE @LowDelta  decimal(19,4) = CASE WHEN @LowId  = @FromId THEN -@Amount ELSE @Amount END;
    DECLARE @HighDelta decimal(19,4) = CASE WHEN @HighId = @FromId THEN -@Amount ELSE @Amount END;

    BEGIN TRY
        BEGIN TRANSACTION;

        UPDATE dbo.Accounts
           SET Balance = Balance + @LowDelta
         WHERE AccountId = @LowId
           AND (@LowDelta &gt; 0 OR Balance &gt;= @Amount);
        IF @@ROWCOUNT = 0
            THROW 50003, 'Account not found or insufficient funds.', 1;

        UPDATE dbo.Accounts
           SET Balance = Balance + @HighDelta
         WHERE AccountId = @HighId
           AND (@HighDelta &gt; 0 OR Balance &gt;= @Amount);
        IF @@ROWCOUNT = 0
            THROW 50003, 'Account not found or insufficient funds.', 1;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT &gt; 0 ROLLBACK TRANSACTION;
        THROW;
    END CATCH
END;</code></pre>
<p>The procedure rejects zero, negative and NULL amounts, and it rejects a transfer from an account to itself. If either update finds no row to change, the transaction rolls back, so a failed debit never leaves a half-finished credit behind. <code>XACT_ABORT ON</code> also rolls back the whole transaction on any runtime error.</p>
<p>The fixed order only protects code that follows it. Any other procedure that updates the same accounts in a different order, or a trigger that touches other rows, can still deadlock with this one. Keep the retry logic for error 1205 in place.</p>
<h2 id="connection-management-and-microsoft-data-sqlclient-best-prac">Connection Management and Microsoft.Data.SqlClient Best Practices</h2>
<p>Use Microsoft.Data.SqlClient (v7.0.1 is shown in the current API docs) and keep the defaults <code>Encrypt=Mandatory</code> and <code>TrustServerCertificate=false</code>. When an app starts failing with certificate errors after a driver upgrade, the driver is not broken. The client simply does not trust the server certificate. Fix the certificate chain or the host name instead of switching on <code>TrustServerCertificate</code>. This is one of the most useful SQL Server best practices for developers.</p>
<p>The animated diagram below shows how the client decides whether a connection succeeds and which fix to choose.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/sql-server-best-practices-guidelines-diagram-6.gif" alt="Decision flow from opening a connection through Encrypt settings and certificate checks to a connected result or a certificate error with two fixes." width="1200" height="675" /><figcaption>Certificate errors come from failed server identity checks; fix the certificate and host name rather than enabling TrustServerCertificate.</figcaption></figure>
<h3>Encrypt Options Compared</h3>
<p><code>Encrypt=Strict</code> uses TDS 8.0 and cannot be bypassed with <code>TrustServerCertificate</code>. Use it when your server supports TDS 8.0. Avoid <code>TrustServerCertificate=true</code> in production, because it skips the server identity check.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Encrypt value</th>
<th>Behaviour</th>
<th>Certificate validation</th>
<th>Use when</th>
</tr>
</thead>
<tbody>
<tr>
<td>Optional</td>
<td>Encrypts only if the server requires it</td>
<td>Only when encryption is used</td>
<td>Legacy apps during migration, not new work</td>
</tr>
<tr>
<td>Mandatory (default)</td>
<td>Always encrypts</td>
<td>Yes, unless <code>TrustServerCertificate=true</code></td>
<td>Most production apps</td>
</tr>
<tr>
<td>Strict</td>
<td>TDS 8.0, TLS first</td>
<td>Always, cannot be bypassed</td>
<td>Server supports TDS 8.0</td>
</tr>
</tbody>
</table>
</figure>
<h3>Connection Pooling Rules</h3>
<ul>
<li><strong>Open late, close early:</strong> Use <code>using</code> or <code>await using</code> so the connection returns to the pool even when an exception occurs.</li>
<li><strong>One connection per thread:</strong> A <code>SqlConnection</code> is not thread-safe, so never share it.</li>
<li><strong>Keep connection strings identical:</strong> Each distinct string creates its own pool. Per-request values (e.g. a user name in the string) fragment the pool.</li>
<li><strong>Watch Max Pool Size:</strong> The default is 100. Pool exhaustion shows up as &#8220;timeout period elapsed prior to obtaining a connection from the pool&#8221;. The usual cause is a leaked connection, so look for missing disposal before raising the limit.</li>
</ul>
<h3>Availability Groups and Read-Only Routing</h3>
<p>Connect to the availability group listener name, never a node name. Add <code>MultiSubnetFailover=True</code> when the listener spans subnets, so the client tries all IP addresses in parallel. Add <code>ApplicationIntent=ReadOnly</code> for reporting workloads that should use read-only routing.</p>
<h3>C# Example: Strict Encryption and a Typed Parameter</h3>
<p>Example: a retail app looks up orders for one customer. The parameter has an explicit type and size, which avoids implicit conversions and plan cache bloat.</p>
<p class="code-label"><strong>C# (.NET)</strong></p>
<pre><code class="language-csharp">using System.Data;
using Microsoft.Data.SqlClient;

var builder = new SqlConnectionStringBuilder
{
    DataSource = "orders-listener.contoso.com",
    InitialCatalog = "Sales",
    IntegratedSecurity = true,
    Encrypt = SqlConnectionEncryptOption.Strict,
    MultiSubnetFailover = true,
    ApplicationIntent = ApplicationIntent.ReadWrite,
    MaxPoolSize = 100
};

await using var connection = new SqlConnection(builder.ConnectionString);
await connection.OpenAsync();

await using var command = new SqlCommand(
    "SELECT OrderId, OrderDate FROM dbo.Orders WHERE CustomerRef = @CustomerRef;",
    connection);
command.Parameters.Add("@CustomerRef", SqlDbType.VarChar, 20).Value = "C-10045";

await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    Console.WriteLine($"{reader.GetInt32(0)} {reader.GetDateTime(1):d}");
}</code></pre>
<p>This opens a strict, encrypted connection and returns it to the pool when the block ends.</p>
<p>For transient errors, the driver offers configurable retry logic (<code>SqlRetryLogicOptions</code> with <code>SqlConfigurableRetryFactory</code>). Assign a provider to the connection or command, and keep the retry count and delay bounded.</p>
<h3>Python Equivalent with pyodbc</h3>
<p>Use the ODBC Driver for SQL Server, set <code>Encrypt=yes</code> and <code>TrustServerCertificate=no</code>, and pass values through <code>?</code> placeholders. Match the driver name to the one installed on your machine.</p>
<p class="code-label"><strong>Python</strong></p>
<pre><code class="language-python">import os
import pyodbc

conn_str = (
    "DRIVER={ODBC Driver 18 for SQL Server};"
    "SERVER=orders-listener.contoso.com;"
    "DATABASE=Sales;"
    f"UID={os.environ['SQL_USER']};PWD={os.environ['SQL_PASSWORD']};"
    "Encrypt=yes;TrustServerCertificate=no;"
    "MultiSubnetFailover=Yes;"
)

with pyodbc.connect(conn_str) as conn:
    cursor = conn.cursor()
    cursor.execute(
        "SELECT OrderId, OrderDate FROM dbo.Orders WHERE CustomerRef = ?",
        "C-10045",
    )
    for row in cursor.fetchall():
        print(row.OrderId, row.OrderDate)</code></pre>
<p>This keeps the password out of the source code and sends the value as a parameter, not as part of the SQL text.</p>
<h2 id="sql-server-security-best-practices-least-privilege-injection">SQL Server Security Best Practices: Least Privilege, Injection and Encryption</h2>
<p>Use Windows or Microsoft Entra authentication, give each application its own login with only the permissions it needs, and never connect an application as <code>sa</code> or as a member of <code>db_owner</code>. These SQL Server security best practices limit the damage if one credential or one query is compromised.</p>
<h3>Apply the Least Privilege Pattern</h3>
<p>Create a database role for the application, grant it <code>EXECUTE</code> on a schema, and give it no direct table access. Stored procedures and tables owned by the same principal use ownership chaining, so the procedure can read the tables even though the caller cannot.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Permission model</th>
<th>Risk</th>
<th>Use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>db_owner</code></td>
<td>Full control of the database, including dropping objects and changing permissions</td>
<td>DBAs and deployment accounts only</td>
</tr>
<tr>
<td><code>db_datareader</code> / <code>db_datawriter</code></td>
<td>Read or write every table, including future ones. An injection flaw exposes all data</td>
<td>Simple tools and trusted internal jobs</td>
</tr>
<tr>
<td>Custom role with <code>EXECUTE</code> on a schema</td>
<td>Limited to the procedures in that schema</td>
<td>Default for application connections</td>
</tr>
</tbody>
</table>
</figure>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">CREATE LOGIN [CORP\svc-orders-app] FROM WINDOWS;
GO
USE OrdersDb;
GO
CREATE SCHEMA app AUTHORIZATION dbo;
GO
CREATE USER [CORP\svc-orders-app] FOR LOGIN [CORP\svc-orders-app];
CREATE ROLE app_orders_role;
GRANT EXECUTE ON SCHEMA::app TO app_orders_role;
ALTER ROLE app_orders_role ADD MEMBER [CORP\svc-orders-app];</code></pre>
<p>This creates the login, the user, and a role that can only run procedures in the <code>app</code> schema.</p>
<h3>Prevent SQL Injection in Dynamic SQL</h3>
<p>Build dynamic SQL with parameters through <code>sp_executesql</code> and never concatenate user input into the query text. Microsoft guidance also says to review every piece of code that calls <code>EXEC</code>, <code>EXECUTE</code> or <code>sp_executesql</code>. Dynamic SQL also breaks ownership chaining, so the caller needs permissions on the underlying tables.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">DECLARE @sql nvarchar(max) =
    N'SELECT OrderId, OrderDate, Total
      FROM dbo.Orders
      WHERE CustomerId = @CustomerId AND OrderDate &gt;= @From;';

EXEC sys.sp_executesql
     @sql,
     N'@CustomerId int, @From date',
     @CustomerId = 42,
     @From = '2026-01-01';

-- Object names cannot be parameters, so quote them
DECLARE @table sysname = N'Orders';
SET @sql = N'SELECT COUNT(*) FROM dbo.' + QUOTENAME(@table) + N';';
EXEC sys.sp_executesql @sql;</code></pre>
<p>Typed parameters also give you plan reuse. <code>QUOTENAME</code> is for object names only, and you should still check them against an allow-list.</p>
<h3>Encrypt Data in Transit, at Rest and in Columns</h3>
<ul>
<li><strong>In transit:</strong> Use TLS with a valid server certificate. See the connection management section for <code>Encrypt</code> settings.</li>
<li><strong>At rest:</strong> Enable Transparent Data Encryption (TDE) to protect data files and backups from a stolen disk or file copy. TDE does not protect against a user who has permission to query the data.</li>
<li><strong>Sensitive columns:</strong> Use Always Encrypted when even DBAs should not read the values. The keys stay with the client.</li>
<li><strong>Backups:</strong> Encrypt them, and store the certificate or key separately. Without it, the backup cannot be restored.</li>
</ul>
<h3>Audit Activity and Reduce the Attack Surface</h3>
<p>Use SQL Server Audit to write to a file or the Windows event log. Capture logins, permission changes and access to sensitive tables. Disable or rename <code>sa</code> if you don&#8217;t need it, and keep <code>xp_cmdshell</code> and other unused features turned off. This is a core item on any SQL Server DBA checklist.</p>
<h3>Example: Patient Data in a Healthcare System</h3>
<p>Give the reporting team a read-only role with column-level permissions, so identifying columns stay out of reach.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">CREATE ROLE reporting_role;
GRANT SELECT ON dbo.Patient (PatientId, AdmissionDate, WardCode) TO reporting_role;
DENY  SELECT ON dbo.Patient (FullName, NationalId) TO reporting_role;</code></pre>
<p>Dynamic data masking can hide values in query results, but it is a convenience and not a security boundary. A user who can filter or infer values can still work around it, so rely on permissions and encryption.</p>
<h2 id="sql-server-backup-best-practices-high-availability-and-resto">SQL Server Backup Best Practices, High Availability and Restore Testing</h2>
<p>A backup is only proven when you have restored it. Match the recovery model to your recovery point objective (RPO, the amount of data loss you can accept), take full, differential and log backups, keep copies off the server, and test restores on a schedule. These are the core SQL Server backup best practices.</p>
<h3>Choose a Backup Strategy That Matches Your RPO and RTO</h3>
<p>The recovery model decides which backups are possible. RTO means recovery time objective, the time you can afford to be down.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Strategy</th>
<th>RPO</th>
<th>RTO</th>
<th>Log growth risk</th>
</tr>
</thead>
<tbody>
<tr>
<td>Full only (SIMPLE recovery)</td>
<td>Back to the last full backup</td>
<td>Short, one restore</td>
<td>Low, the log is reused automatically</td>
</tr>
<tr>
<td>Full + differential (SIMPLE)</td>
<td>Back to the last differential</td>
<td>Short, two restores</td>
<td>Low</td>
</tr>
<tr>
<td>Full + differential + log (FULL recovery)</td>
<td>Minutes, set by log backup interval</td>
<td>Longer, a full chain plus point-in-time options</td>
<td>High if log backups stop</td>
</tr>
</tbody>
</table>
</figure>
<p>In FULL recovery, SQL Server keeps log records until a log backup has captured them. With no log backups, the log cannot be reused, so it grows until the disk is full. Switch to SIMPLE if you do not need point-in-time recovery.</p>
<p>Example: a retail orders database takes a full backup nightly, a differential every four hours and a log backup every 10 minutes. That gives an example RPO of about 10 minutes. It is an illustration, not a recommendation, so set your own interval from business needs.</p>
<h3>Use CHECKSUM, Compression and Encryption on Every Backup</h3>
<p>CHECKSUM detects page corruption while the backup runs. Compression reduces size and I/O. Encryption protects the file once it leaves the server.</p>
<p><code>RESTORE VERIFYONLY</code> is the minimum check. It confirms the backup is readable, but it does not prove the data is consistent. Only a real restore followed by <code>DBCC CHECKDB</code> on the restored copy does that.</p>
<h3>ZSTD Compression and Secondary Backups in SQL Server 2025</h3>
<p>ZSTD backup compression is GA in SQL Server 2025 but is not the default. Enable it per backup with <code>COMPRESSION (ALGORITHM = ZSTD, LEVEL = LOW | MEDIUM | HIGH)</code>, or instance-wide by setting <code>backup compression algorithm</code> to 3 with <code>sp_configure</code>. Test CPU cost and backup time first, because HIGH can take much longer.</p>
<p>Full, differential and log backups can now run on availability group secondary replicas (GA), which takes load off the primary. Backups to immutable storage are also GA, which helps against ransomware.</p>
<p>The animated diagram below shows the full path from backup to a verified restore.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/sql-server-best-practices-guidelines-diagram-8.gif" alt="Pipeline: primary or AG secondary emits backups through a compress, encrypt, CHECKSUM gate to local and offsite copies, then restore test and CHECKDB." width="1200" height="675" /><figcaption>A backup is proven only when a scheduled restore plus DBCC CHECKDB succeeds, and the restore time gives your measured RTO.</figcaption></figure>
<h3>High Availability Is Not a Backup</h3>
<p>An availability group replicates a bad <code>DELETE</code> or corruption to the secondary just as quickly. Use HA for uptime and backups for recovery.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Option</th>
<th>Scope</th>
<th>Failover</th>
<th>Readable secondary</th>
<th>Typical use</th>
</tr>
</thead>
<tbody>
<tr>
<td>Always On availability group</td>
<td>Database(s)</td>
<td>Automatic or manual</td>
<td>Yes</td>
<td>HA, read offload, backups on secondary</td>
</tr>
<tr>
<td>Failover cluster instance</td>
<td>Whole instance</td>
<td>Automatic, shared storage</td>
<td>No</td>
<td>Instance-level HA, jobs and logins included</td>
</tr>
<tr>
<td>Log shipping</td>
<td>Database</td>
<td>Manual</td>
<td>Optional, in standby mode</td>
<td>Simple DR to another site</td>
</tr>
</tbody>
</table>
</figure>
<h3>Protect Copies, Credentials and Keys</h3>
<p>Keep one copy on a local share for fast restores and one offsite or on immutable storage. Document the backup credential, certificate and private key, and store them separately from the backups. If you lose the certificate, encrypted backups cannot be read.</p>
<h3>Backup and Restore Test Script</h3>
<p>The script below backs up, restores to a test name with <code>MOVE</code>, and checks consistency. The test server needs the same certificate. Run <code>RESTORE FILELISTONLY</code> first to get the logical file names.</p>
<p class="code-label"><strong>SQL (T-SQL)</strong></p>
<pre><code class="language-sql">-- Optional: make ZSTD the instance default (SQL Server 2025)
EXEC sp_configure 'backup compression algorithm', 3;
RECONFIGURE;

BACKUP DATABASE SalesDB
TO DISK = N'D:\Backup\SalesDB_full.bak'
WITH CHECKSUM,
     COMPRESSION (ALGORITHM = ZSTD, LEVEL = MEDIUM),
     ENCRYPTION (ALGORITHM = AES_256, SERVER CERTIFICATE = BackupCert),
     STATS = 10;

BACKUP LOG SalesDB
TO DISK = N'D:\Backup\SalesDB_log.trn'
WITH CHECKSUM,
     COMPRESSION (ALGORITHM = ZSTD, LEVEL = LOW),
     ENCRYPTION (ALGORITHM = AES_256, SERVER CERTIFICATE = BackupCert);

-- On the restore test server
RESTORE FILELISTONLY FROM DISK = N'D:\Backup\SalesDB_full.bak';

RESTORE DATABASE SalesDB_RestoreTest
FROM DISK = N'D:\Backup\SalesDB_full.bak'
WITH MOVE N'SalesDB'     TO N'E:\RestoreTest\SalesDB_RestoreTest.mdf',
     MOVE N'SalesDB_log' TO N'F:\RestoreTest\SalesDB_RestoreTest.ldf',
     CHECKSUM, RECOVERY, STATS = 10;

DBCC CHECKDB (SalesDB_RestoreTest) WITH NO_INFOMSGS, ALL_ERRORMSGS;</code></pre>
<p>This restores the full backup under a new name and reports any corruption. Record how long the restore took, because that is your measured RTO.</p>
<p>For a point-in-time restore, restore the full backup (and the latest differential) with <code>NORECOVERY</code>. Then restore each log backup, and add <code>STOPAT = '2026-10-13T09:55:00'</code> to the last one with <code>RECOVERY</code>. This needs FULL recovery and an unbroken log chain.</p>
<h2 id="monitoring-sql-server-wait-stats-extended-events-and-perform">Monitoring SQL Server: Wait Stats, Extended Events and Performance Counters</h2>
<p>Start with wait statistics to see what the instance is waiting on. Then use Query Store and Extended Events to find the queries and events behind those waits. This order is the core of SQL Server performance tuning best practices, because it stops you from guessing.</p>
<h3>Common Wait Types and What They Point To</h3>
<p>A wait type is the reason a request had to pause. Treat it as a pointer, not a diagnosis. Confirm it with a query, a plan or a counter before you change anything.</p>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Wait</th>
<th>Usually points to</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>PAGEIOLATCH_*</code></td>
<td>Slow data storage or memory pressure</td>
</tr>
<tr>
<td><code>LCK_M_*</code></td>
<td>Blocking between sessions</td>
</tr>
<tr>
<td><code>WRITELOG</code></td>
<td>Slow transaction log storage</td>
</tr>
<tr>
<td><code>CXPACKET</code> / <code>CXCONSUMER</code></td>
<td>Parallelism (check MAXDOP and cost threshold)</td>
</tr>
<tr>
<td><code>RESOURCE_SEMAPHORE</code></td>
<td>Queries waiting for memory grants</td>
</tr>
</tbody>
</table>
</figure>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">SELECT TOP (10)
       wait_type,
       waiting_tasks_count,
       wait_time_ms,
       signal_wait_time_ms,
       CAST(100.0 * wait_time_ms / SUM(wait_time_ms) OVER () AS decimal(5,2)) AS pct_of_waits
FROM sys.dm_os_wait_stats
WHERE wait_time_ms &gt; 0
  AND wait_type NOT IN (N'SLEEP_TASK', N'LAZYWRITER_SLEEP', N'XE_TIMER_EVENT',
      N'XE_DISPATCHER_WAIT', N'REQUEST_FOR_DEADLOCK_SEARCH', N'CHECKPOINT_QUEUE',
      N'DIRTY_PAGE_POLL', N'LOGMGR_QUEUE', N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
      N'SP_SERVER_DIAGNOSTICS_SLEEP', N'BROKER_TO_FLUSH', N'BROKER_TASK_STOP',
      N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', N'QDS_ASYNC_QUEUE', N'WAITFOR')
ORDER BY wait_time_ms DESC;</code></pre>
<p>This returns the top waits since the last restart, minus a partial list of idle waits. Take two snapshots and compare them to see current behaviour.</p>
<h3>Use Extended Events, Not Profiler</h3>
<p>Extended Events is the lightweight tracing system, and SQL Trace and Profiler are deprecated. Keep the built-in <code>system_health</code> session running for deadlock graphs. Add a small session for slow statements.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">CREATE EVENT SESSION [LongRunningStatements] ON SERVER
ADD EVENT sqlserver.sql_statement_completed
(
    ACTION (sqlserver.database_name, sqlserver.client_app_name, sqlserver.sql_text)
    WHERE duration &gt; 1000000   -- microseconds, so 1 second
)
ADD TARGET package0.event_file
(
    SET filename = N'LongRunningStatements.xel', max_file_size = 50, max_rollover_files = 4
)
WITH (MAX_DISPATCH_LATENCY = 30 SECONDS, STARTUP_STATE = ON);

ALTER EVENT SESSION [LongRunningStatements] ON SERVER STATE = START;</code></pre>
<p>The file target rolls over at a fixed size, so the session cannot fill the disk.</p>
<h3>Counters and Alerts Worth Setting Up</h3>
<ul>
<li><strong>Page life expectancy:</strong> Watch the trend against your own baseline. A fixed number such as 300 does not fit every server.</li>
<li><strong>Batch requests per second:</strong> Use it to read load changes next to waits.</li>
<li><strong>Log file percent used:</strong> Alert before the log fills the volume.</li>
<li><strong>Free space:</strong> Check data, log and tempdb volumes separately.</li>
<li><strong>AG synchronization lag:</strong> Alert when secondaries fall behind.</li>
<li><strong>Errors and jobs:</strong> Alert on severity 17 to 25 errors and on job failures.</li>
</ul>
<p>For backups, alert on the age of the last successful backup, not on whether the job ran. A job can finish without producing a usable backup. This also belongs on any SQL Server DBA checklist.</p>
<h2 id="patching-and-upgrading-to-sql-server-2025-what-to-check-firs">Patching and Upgrading to SQL Server 2025: What to Check First</h2>
<p>Patch on a regular cadence, test the upgrade on a copy that has the same linked servers, certificates and jobs, and plan the outage window before you start. Most upgrade surprises come from things outside the database files.</p>
<h3>Version, Support and Current Builds</h3>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Version</th>
<th>Mainstream support</th>
<th>Extended support</th>
<th>Latest build (October 2026)</th>
</tr>
</thead>
<tbody>
<tr>
<td>SQL Server 2019</td>
<td>Ended 28 February 2025</td>
<td>Until 8 January 2030</td>
<td>CU32 15.0.4430.1; CU32+GDR 15.0.4490.9</td>
</tr>
<tr>
<td>SQL Server 2022</td>
<td>Ends 11 January 2028</td>
<td>Check the Microsoft lifecycle page for the end date</td>
<td>CU27 16.0.4295.3</td>
</tr>
<tr>
<td>SQL Server 2025 (GA 18 November 2025, RTM 17.0.1000.7)</td>
<td>Ends 6 January 2031</td>
<td>Check the Microsoft lifecycle page for the end date</td>
<td>CU9 17.0.5005.3 (15 September 2026); GDR 17.0.1135.8; CU8+GDR 17.0.4085.5</td>
</tr>
</tbody>
</table>
</figure>
<p>A CU (cumulative update) carries fixes and features. A GDR (General Distribution Release) carries security-only fixes for servers that don&#8217;t apply CUs. Each instance sits on one of two tracks, and the rule is one-way.</p>
<ol>
<li>GDR track: the instance has only had GDR updates and receives security fixes only (e.g., 17.0.1135.8 for SQL Server 2025).</li>
<li>CU track: the instance has had at least one CU. From here, security-only fixes arrive as a CU+GDR package built on the CU you already have (e.g., CU8+GDR 17.0.4085.5).</li>
<li>Moving from GDR to CU is allowed. Install a CU and the instance joins the CU track.</li>
<li>Moving from CU back to GDR is not allowed. Once a CU is installed, only CU and CU+GDR packages apply.</li>
</ol>
<p>Decide the track before the first patch. If you may want bug fixes or features later, plan to move to the CU track once and stay there. If you only want security fixes, stay on GDR until you are ready to commit to CUs.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">SELECT SERVERPROPERTY('ProductVersion') AS ProductVersion,
       SERVERPROPERTY('ProductLevel')   AS ProductLevel,
       SERVERPROPERTY('Edition')        AS Edition;</code></pre>
<p>Compare the returned build number with the table above to see which track and update level the instance is on.</p>
<h3>Is the Upgrade Worth It?</h3>
<p>Yes for write-heavy or backup-heavy systems, after testing. The practical 2025 gains are:</p>
<ul>
<li>Optimized locking, which uses transaction ID (TID) locking and lock after qualification (LAQ) to reduce blocking and lock memory, and to avoid lock escalation, with no application code changes.</li>
<li>Query Store on readable secondaries, on by default (in 2022 it was off by default and needed trace flag 12606).</li>
<li>ZSTD backup compression and backups on AG secondaries.</li>
<li>Resource Governor and higher limits in Standard edition.</li>
<li>tempdb space governance.</li>
</ul>
<p>The trade-off is testing effort. DiskANN vector indexes are preview only, so keep them out of production.</p>
<h3>What Breaks: Linked Servers and TLS</h3>
<p>A DBA report found that linked servers using SQLNCLI or MSOLEDBSQL can fail after an in-place upgrade to 2025, because of stricter TLS certificate validation. Setup can also restart the service up to three times, so the outage is longer than one restart. Fix the certificates or the provider settings before cutover, then test every linked server afterwards.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">DECLARE @name sysname;
DECLARE ls CURSOR FAST_FORWARD FOR
    SELECT name FROM sys.servers WHERE is_linked = 1;
OPEN ls;
FETCH NEXT FROM ls INTO @name;
WHILE @@FETCH_STATUS = 0
BEGIN
    BEGIN TRY
        EXEC sp_testlinkedserver @name;
        PRINT @name + ': OK';
    END TRY
    BEGIN CATCH
        PRINT @name + ': FAILED - ' + ERROR_MESSAGE();
    END CATCH;
    FETCH NEXT FROM ls INTO @name;
END;
CLOSE ls;
DEALLOCATE ls;</code></pre>
<p>The animated diagram below shows the safe order of steps, including the retry loop for failed linked server tests.</p>
<h3>In-Place vs Side-by-Side Migration</h3>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Approach</th>
<th>Rollback</th>
<th>Downtime</th>
<th>Effort</th>
</tr>
</thead>
<tbody>
<tr>
<td>In-place</td>
<td>Hard; restore the old setup from backups</td>
<td>Longer (service restarts during Setup)</td>
<td>Low</td>
</tr>
<tr>
<td>Side-by-side (backup/restore or log shipping cutover)</td>
<td>Easy; old server stays intact</td>
<td>Short, limited to the cutover</td>
<td>Higher</td>
</tr>
</tbody>
</table>
</figure>
<p>Use side-by-side for critical systems. Use in-place only for small or non-critical instances where a longer window is acceptable.</p>
<h3>After the Upgrade: Compatibility Level</h3>
<p>Keep the old compatibility level until the application is tested. Let Query Store capture baseline plans, then raise the level and force back any plan that regressed.</p>
<p>The level used below, 170 for SQL Server 2025, is not in the verified research for this article. Confirm it in the Microsoft documentation article &#8220;ALTER DATABASE compatibility level (Transact-SQL)&#8221; before you run the statement, and use the highest level that article lists for your version.</p>
<p class="code-label"><strong>T-SQL</strong></p>
<pre><code class="language-sql">-- Check the current level
SELECT name, compatibility_level FROM sys.databases WHERE name = N'SalesDb';

-- Raise it after testing. Replace 170 if the Microsoft docs list a different
-- level for your SQL Server 2025 build.
ALTER DATABASE SalesDb SET COMPATIBILITY_LEVEL = 170;</code></pre>
<h2 id="common-anti-patterns-and-sql-server-dba-production-readiness">Common Anti-Patterns and SQL Server DBA Production Readiness Checklist</h2>
<p>Most production incidents trace back to a small set of repeated mistakes. Use the table below to spot them, then run the checklist before a server goes live.</p>
<h3>Common SQL Server Anti-Patterns</h3>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Anti-pattern</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default <code>max server memory</code></td>
<td>OS paging, memory pressure on the instance</td>
<td>Cap it, starting near 75% of available memory</td>
</tr>
<tr>
<td>MAXDOP 1 everywhere, or default 0 on a large NUMA box</td>
<td>Slow reports and index builds, or heavy parallel waits</td>
<td>Start from the NUMA-based guidance, scope per database</td>
</tr>
<tr>
<td>Cost threshold left at 5</td>
<td>Tiny queries going parallel</td>
<td>Raise it from observed plan costs, then check CXPACKET/CXCONSUMER</td>
</tr>
<tr>
<td>Single tempdb file, small autogrow steps</td>
<td>PAGELATCH contention, pauses during growth</td>
<td>Equally sized, pre-sized files with fixed growth</td>
</tr>
<tr>
<td>Clustered key on <code>NEWID()</code> GUID</td>
<td>Page splits, fragmentation</td>
<td>Use an ever-increasing key or <code>NEWSEQUENTIALID</code></td>
</tr>
<tr>
<td>Functions on indexed columns in <code>WHERE</code></td>
<td>Index scans instead of seeks</td>
<td>Rewrite as SARGable range predicates</td>
</tr>
<tr>
<td><code>SELECT *</code></td>
<td>Extra I/O, covering indexes unused</td>
<td>List only the needed columns</td>
</tr>
<tr>
<td>String-built dynamic SQL</td>
<td>Injection risk, plan cache bloat</td>
<td>Use <code>sp_executesql</code> with typed parameters</td>
</tr>
<tr>
<td>Apps connecting as <code>sa</code> or <code>db_owner</code></td>
<td>Large blast radius after a compromise</td>
<td>Custom role with <code>EXECUTE</code> on a schema</td>
</tr>
<tr>
<td><code>TrustServerCertificate=true</code> in production</td>
<td>No server identity check</td>
<td>Fix the certificate chain and host name</td>
</tr>
<tr>
<td>FULL recovery with no log backups</td>
<td>Log file grows until the disk fills</td>
<td>Schedule log backups, or use SIMPLE if the RPO allows</td>
</tr>
<tr>
<td>Backups never restored</td>
<td>Surprise failure at recovery time</td>
<td>Scheduled test restore plus <code>DBCC CHECKDB</code></td>
</tr>
<tr>
<td>Linked servers not tested after upgrade</td>
<td>Failures after cutover</td>
<td>Run <code>sp_testlinkedserver</code> on a copy first</td>
</tr>
</tbody>
</table>
</figure>
<h3>SQL Server Production Readiness Checklist</h3>
<p>This SQL Server DBA checklist combines the configuration, security and backup rules from the sections above. Work through it in order.</p>
<ol>
<li>Confirm the version and patch level, and choose the CU or GDR track on purpose.</li>
<li>Set the SQL Server max server memory setting to a specific value, with <code>min server memory</code> at 0.</li>
<li>Set MAXDOP and cost threshold for parallelism from the NUMA rule and measured plan costs.</li>
<li>Lay out tempdb with equally sized, pre-sized files on fast storage.</li>
<li>Separate data, log and tempdb volumes where the storage design allows.</li>
<li>Enable instant file initialization for data files.</li>
<li>Enable Query Store on user databases with a sensible size and capture mode.</li>
<li>Choose the isolation level deliberately, and test RCSI before turning it on.</li>
<li>Set connection strings with <code>Encrypt</code>, the listener name and <code>MultiSubnetFailover=True</code> where it applies.</li>
<li>Create least-privilege roles for each application, and disable or rename <code>sa</code>.</li>
<li>Enforce TLS in transit and enable TDE at rest.</li>
<li>Turn on SQL Server Audit for logins, permission changes and sensitive tables.</li>
<li>Schedule full, differential and log backups with <code>CHECKSUM</code>, compression and encryption.</li>
<li>Keep an offsite or immutable copy, and document backup keys and certificates.</li>
<li>Restore on a schedule and run <code>DBCC CHECKDB</code> on the restored copy.</li>
<li>Alert on wait trends, failed jobs, severity 17-25 errors and the age of the last good backup.</li>
<li>Write the upgrade plan, including linked server tests and the outage window.</li>
</ol>
<p>These items also form a short SQL Server security best practices review (items 10 to 12) and a SQL Server backup best practices review (items 13 to 15).</p>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li><strong>Cap max server memory:</strong> Start from about 75% of the memory that is available to SQL Server (after other processes and other instances), keep min server memory at 0, and always use a specific value if Lock Pages in Memory is granted.</li>
<li><strong>Tune MAXDOP and cost threshold by measurement:</strong> Begin with the Microsoft NUMA-based MAXDOP guidance, use database-scoped MAXDOP or Resource Governor for exceptions, and raise cost threshold for parallelism above the default of 5 after checking CXPACKET/CXCONSUMER waits.</li>
<li><strong>Use Strict or Mandatory encryption with a valid certificate:</strong> Fix the certificate chain or host name when connections fail after a driver upgrade. Avoid <code>TrustServerCertificate=true</code> in production.</li>
<li><strong>Give applications least privilege:</strong> Use a dedicated login and a role with EXECUTE on a schema, never <code>sa</code> or <code>db_owner</code>, and pass every value through typed parameters.</li>
<li><strong>Enable Query Store:</strong> Turn it on for every database you care about, so you can find regressions, force a known good plan, and keep baseline plans before a compatibility level change.</li>
<li><strong>Test restores, not just backups:</strong> Take backups with CHECKSUM, keep an offsite or immutable copy, and restore to a test name regularly, then run DBCC CHECKDB on the restored copy.</li>
<li><strong>Test the SQL Server 2025 upgrade on a copy first:</strong> Include linked servers, certificates and jobs, and run <code>sp_testlinkedserver</code> afterwards because stricter TLS validation can break them. Evaluate optimized locking with RCSI for write-heavy workloads, and keep DiskANN vector indexes out of production while they are preview only.</li>
</ul>
<p>That&#8217;s all! Happy coding!</p>
<p>Did this guide help you to set up and tune SQL Server for production? Please share your questions or experience in the comments below.</p>
<p>Bookmark this page for your next production review and share it with your team. Subscribe to TheCodeBuzz to get new database best-practice guides in your inbox.</p>
<h2 id="faq">FAQ</h2>
<h3>What should I set max server memory to in SQL Server?</h3>
<p>Start with about 75% of the memory that is available to the instance, meaning the total minus what the OS, other instances and other processes use, and leave min server memory at 0. The default is about 2 TB, which is effectively unlimited, so you should cap it on every version. If Lock Pages in Memory is granted to the service account, you must set a specific value, not the default. Set it with <code>sp_configure 'max server memory (MB)'</code> followed by <code>RECONFIGURE</code>, and check the result in <code>sys.configurations</code>.</p>
<h3>What is the best MAXDOP and cost threshold for parallelism setting?</h3>
<p>There is no single best value. Start from Microsoft&#8217;s NUMA-based MAXDOP guidance, set it per database with <code>ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP</code> for mixed workloads, and use Resource Governor workload groups where one workload needs a different limit. Avoid MAXDOP 1 server-wide, because it breaks parallel index builds and slows reporting queries. The cost threshold for parallelism defaults to 5, which is too low for modern hardware, so raise it based on observed plan costs and check the effect on CXPACKET and CXCONSUMER waits.</p>
<h3>How many tempdb files should I have, and how should I size them?</h3>
<p>Start with a small number of equally sized data files, up to the number of cores as Microsoft guidance suggests, and add more only if monitoring shows allocation contention. Pre-size the files and use a fixed growth increment, so autogrow stays an emergency measure and not a routine event. Equal sizes matter because SQL Server spreads allocations across files, and uneven files lead to uneven use. On SQL Server 2025 you can also use Resource Governor tempdb space governance, which stops a workload with an error when it exceeds its limit.</p>
<h3>Why does my app fail to connect after upgrading Microsoft.Data.SqlClient, with certificate errors?</h3>
<p>The driver now defaults to <code>Encrypt=Mandatory</code> and <code>TrustServerCertificate=false</code>, so it rejects a server certificate that the client does not trust. This is not a driver bug. Fix the certificate chain or make sure the host name in the connection string matches the certificate, instead of switching on <code>TrustServerCertificate=true</code>, which skips server identity checks. Use <code>Encrypt=Strict</code> where the server supports TDS 8.0, because Strict cannot be bypassed with <code>TrustServerCertificate</code>.</p>
<h3>Should I enable Query Store, optimized locking or Read Committed Snapshot Isolation?</h3>
<p>Yes to Query Store on every user database you care about, because it helps you find regressions and force a known good plan. Enable Read Committed Snapshot Isolation (RCSI) after testing, since it adds version storage and changes behavior for code that relied on blocking reads. Optimized locking comes with SQL Server 2025 and needs no application code changes. It works best together with RCSI, so it is a good reason to test 2025 for heavy write workloads.</p>
<h2 id="sources">Sources and further reading</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/sql/sql-server/what-s-new-in-sql-server-2025?view=sql-server-ver17" target="_blank" rel="noopener">What&#8217;s New in SQL Server 2025 &#8211; SQL Server</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/troubleshoot/sql/releases/download-and-install-latest-updates" target="_blank" rel="noopener">Latest Updates and Version History for SQL Server</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/server-memory-server-configuration-options?view=sql-server-ver17" target="_blank" rel="noopener">Server Memory Configuration Options &#8211; SQL Server</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/memory-management-architecture-guide?view=sql-server-ver17" target="_blank" rel="noopener">Memory Management Architecture Guide &#8211; SQL Server</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/resource-governor/resource-governor?view=sql-server-ver17" target="_blank" rel="noopener">Resource Governor &#8211; SQL Server</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/security/sql-server-security-best-practices?view=sql-server-ver17" target="_blank" rel="noopener">SQL Server Security Best Practices &#8211; SQL Server</a> (Microsoft Learn)</li>
<li><a href="https://learn.microsoft.com/en-us/sql/connect/ado-net/encryption-and-certificate-validation?view=sql-server-ver17" target="_blank" rel="noopener">Encryption and Certificate Validation in Microsoft.Data.SqlClient</a> (Microsoft Learn)</li>
<li><a href="https://techcommunity.microsoft.com/blog/azuresqlblog/whats-new-in-the-backuprestore-area-in-sql-server-2025/4474613" target="_blank" rel="noopener">Whats new in the Backup/Restore area in SQL Server 2025</a> (Microsoft Tech Community)</li>
<li><a href="https://www.mssqltips.com/sqlservertip/8290/sql-server-2025-new-features/" target="_blank" rel="noopener">SQL Server 2025 New Features and Enhancements</a> (MSSQLTips)</li>
<li><a href="https://www.sqlservercentral.com/articles/sql-server-2025-backup-compression-algorithm" target="_blank" rel="noopener">SQL Server 2025 Backup Compression Algorithm</a> (SQLServerCentral)</li>
</ul>
<p><em>Diagrams are original illustrations created by and for TheCodeBuzz.</em></p><p>The post <a href="https://thecodebuzz.com/sql-server-best-practices-guidelines/">SQL Server Best Practices and Guidelines-2026</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/sql-server-best-practices-guidelines/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL &#8211; Best Practices for Production</title>
		<link>https://thecodebuzz.com/postgresql-best-practices-production/</link>
					<comments>https://thecodebuzz.com/postgresql-best-practices-production/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 05:31:19 +0000</pubDate>
				<category><![CDATA[Database Best Practices]]></category>
		<category><![CDATA[High Availability]]></category>
		<category><![CDATA[Indexing]]></category>
		<category><![CDATA[pg_upgrade]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30710</guid>

					<description><![CDATA[<p>Production PostgreSQL best practices: patching, upgrades from 14, indexing, UUIDv7 keys, checksums, OAuth and a readiness checklist you can apply today.</p>
<p>The post <a href="https://thecodebuzz.com/postgresql-best-practices-production/">PostgreSQL – Best Practices for Production</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>In our last article, we learned <a href="https://thecodebuzz.com/mongodb-security-guidelines-and-best-practices/">MongoDB specific guidelines</a>. Today in this article , we will learn about PostgreSQL.</p>
<p>&nbsp;</p>
<p>Most PostgreSQL outages and security incidents come from clusters that were never patched or never upgraded, not from exotic bugs. This guide is for engineers, architects and DBAs who run PostgreSQL in production. It is a compact reference you can bookmark and check a cluster against.</p>
<p>&nbsp;</p>
<p>Here is where things stand in October 2026. PostgreSQL 18 is the current stable major, and 18.6, dated 2026-08-13, is its latest minor. PostgreSQL 14 receives its last fixes on 2026-11-12, so anyone still on it has a deadline measured in weeks. I cover patching and upgrades first, because that is where most production risk sits.</p>
<p>After that come the PostgreSQL 18 features that change how you design schemas and indexes, with the reasoning behind each rule so you can judge where it applies to your workload. These PostgreSQL best practices start with a summary table and end with a checklist and FAQ.</p>
<p>&nbsp;</p>
<blockquote>
<p><strong>Version note:</strong> this guide targets PostgreSQL 18 (18.6), and also covers 17.11, 16.15, 15.19 and 14.24 for patching and upgrade planning, with PostgreSQL 19 Beta 4 mentioned only for testing.</p>
<p>&nbsp;</p>
</blockquote>
<h2 id="postgresql-best-practices-at-a-glance">PostgreSQL best practices at a glance</h2>
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Area</th>
<th>Do this</th>
<th>Avoid this</th>
</tr>
</thead>
<tbody>
<tr>
<td>Patching</td>
<td>Run the latest minor of your major: 18.6, 17.11, 16.15, 15.19 or 14.24. Minors contain only bug and security fixes, so they are low-risk, but still test them in staging first. They need a restart.</td>
<td>Pinning old minors. 18.1 fixed CVE-2025-12817 and 18.4 fixed CVE-2026-6575.</td>
</tr>
<tr>
<td>Versions</td>
<td>Run a supported major. PostgreSQL 14 receives its last fixes on November 12, 2026, so plan that upgrade now. Test your workload against PostgreSQL 19 Beta 4 in a non-production environment.</td>
<td>Running any beta in production, or staying on 14 after its end of support.</td>
</tr>
<tr>
<td>Backups and PITR</td>
<td>Take regular base backups and archive WAL continuously (<code>archive_mode</code> plus an archive command or library that reports failures). Rehearse restores to a chosen point in time, and monitor archive lag.</td>
<td>Relying on <code>pg_dump</code> alone for large production systems, or keeping backups you have never restored.</td>
</tr>
<tr>
<td>Replication and failover</td>
<td>Run at least one streaming standby in another failure domain. Use tested, automated failover tooling with fencing of the old primary. Choose <code>synchronous_commit</code> and synchronous standbys deliberately, based on how much data loss you can accept.</td>
<td>Untested manual failover, and abandoned replication slots that make the primary retain WAL until the disk fills.</td>
</tr>
<tr>
<td>Autovacuum and bloat</td>
<td>Keep autovacuum on. Tune per-table thresholds on hot, high-churn tables. Watch dead tuples, table and index size, and the oldest open transaction.</td>
<td>Disabling autovacuum, or leaving sessions idle in a transaction, which stops cleanup of dead rows.</td>
</tr>
<tr>
<td>Connection pooling</td>
<td>Put a pooler such as PgBouncer in front of the database and keep server-side connections to a small, measured number.</td>
<td>Thousands of direct application connections. Also avoid transaction-mode pooling with session state (session-level <code>SET</code>, advisory locks) without checking compatibility.</td>
</tr>
<tr>
<td>Memory and <code>max_connections</code></td>
<td>Start <code>shared_buffers</code> at roughly a quarter of RAM and adjust from measurements. Size <code>work_mem</code> against the worst-case number of concurrent sorts and hashes. Keep <code>max_connections</code> modest and rely on the pooler.</td>
<td>Raising <code>max_connections</code> to hide pool exhaustion, or a large global <code>work_mem</code> multiplied across many sessions.</td>
</tr>
<tr>
<td>TLS and SCRAM</td>
<td>Set <code>password_encryption = scram-sha-256</code>. Enable <code>ssl</code> and use <code>hostssl</code> lines in <code>pg_hba.conf</code> with <code>scram-sha-256</code>.</td>
<td><code>trust</code> rules, unencrypted remote connections and legacy <code>md5</code> hashes.</td>
</tr>
<tr>
<td>Authentication</td>
<td>Where you have an identity provider, configure OAuth in <code>pg_hba.conf</code> and load validators with <code>oauth_validator_libraries</code>.</td>
<td>Shared static passwords.</td>
</tr>
<tr>
<td>Monitoring</td>
<td>Load <code>pg_stat_statements</code> through <code>shared_preload_libraries</code>. Set <code>log_min_duration_statement</code> to a threshold that fits your workload and enable <code>log_lock_waits</code>. Alert on replication lag, archive failures, disk space and connection saturation.</td>
<td>Logging every statement permanently, or finding slow queries only after users report them.</td>
</tr>
<tr>
<td>Primary keys</td>
<td>Use <code>uuidv7()</code> for timestamp-ordered keys.</td>
<td>Random UUIDs as keys on large B-tree indexes, because of poor locality.</td>
</tr>
<tr>
<td>Composite indexes</td>
<td>Order columns for your main queries. PostgreSQL 18 skip scan lets a query omit an equality condition on leading columns. It helps most when the omitted leading column has few distinct values. Confirm with <code>EXPLAIN</code>.</td>
<td>Dropping or reordering existing indexes because skip scan exists, or adding redundant indexes without checking plans.</td>
</tr>
<tr>
<td>Integrity</td>
<td>Use <code>WITHOUT OVERLAPS</code> and <code>PERIOD</code> temporal constraints.</td>
<td>Application-side overlap checks.</td>
</tr>
<tr>
<td>Storage</td>
<td>Keep data checksums, which <code>initdb</code> enables by default in 18. When upgrading a cluster that has none, <code>pg_upgrade</code> needs matching settings, so initialize the new cluster with <code>--no-data-checksums</code>.</td>
<td>Turning checksums off without a reason.</td>
</tr>
<tr>
<td>I/O</td>
<td>Use 18 async I/O (<code>io_method</code>) for reads. Inspect <code>pg_aios</code>. Benchmark on your own storage before changing the default.</td>
<td>Changing <code>io_method</code> on assumptions.</td>
</tr>
</tbody>
</table>
</figure>
<p>Minor releases contain only bug and security fixes, which is why they are low-risk to apply. Low-risk is not no-risk: read the release notes, test against your workload and schedule the restart. The CVE examples above show why you should not defer them for long.</p>
<p>&nbsp;</p>
<p>Key order matters because a B-tree keeps neighbouring values on neighbouring pages. Time-ordered keys append to the right edge of the index, while random keys touch pages all over it, which hurts cache hit rates on large tables.</p>
<p>&nbsp;</p>
<p>Skip scan changes the column-order trade-off but does not remove it. PostgreSQL has to probe the index once for each distinct value of the omitted leading column. With a handful of values that is cheap.</p>
<p>&nbsp;</p>
<p>With millions of values it is not, and an index that leads with the column you actually filter on is still the better choice. Check the plan before you change any existing index.</p>
<h3> </h3>
<h3>Example: uuidv7 key, temporal constraint and a skip scan candidate</h3>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_booking (
    booking_id uuid NOT NULL DEFAULT uuidv7() PRIMARY KEY,
    tenant_id  integer NOT NULL,
    room_id    integer NOT NULL,
    status     text NOT NULL,   -- few distinct values, for example 'confirmed' or 'cancelled'
    during     tstzrange NOT NULL,
    CONSTRAINT no_double_booking
        UNIQUE (room_id, during WITHOUT OVERLAPS)
);

-- Leading column has few distinct values; queries may filter on tenant_id alone.
CREATE INDEX room_booking_status_tenant_idx
    ON room_booking (status, tenant_id);</code></pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>The scalar <code>room_id</code> in the temporal key needs <code>btree_gist</code>. The range column <code>during</code> is what the constraint checks for overlap. The index behind that constraint also starts with <code>room_id</code>, so lookups by room already have an index and you do not need to add another.</p>
<p>&nbsp;</p>
<p>The <code>(status, tenant_id)</code> index is the skip scan candidate. The query below has an equality condition on <code>tenant_id</code> but none on the leading column <code>status</code>. Before PostgreSQL 18, this index could not serve it efficiently. Run it against representative data after an <code>ANALYZE</code>. On a tiny table the planner may prefer a sequential scan, so the <code>SET LOCAL</code> line is for testing only and shows whether the index is usable at all.</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">ANALYZE room_booking;

BEGIN;
SET LOCAL enable_seqscan = off;   -- test only: see whether the index can serve the query

EXPLAIN (ANALYZE, BUFFERS)
SELECT booking_id
FROM room_booking
WHERE tenant_id = 42;

ROLLBACK;</code></pre>
<p>In the plan, look for a scan on <code>room_booking_status_tenant_idx</code> with <code>tenant_id = 42</code> as the index condition, even though the query never mentions <code>status</code>.</p>
<p>PostgreSQL 18 also reports the number of index searches in <code>EXPLAIN ANALYZE</code>. A skip scan shows roughly one search per distinct <code>status</code> value.</p>
<p>Then rerun the query without the <code>SET LOCAL</code> line and compare timing and buffer counts with the plan the planner picks on its own. If the planner chooses a sequential scan or another index and is faster, keep that plan. If <code>status</code> has many distinct values in your data, a separate index on <code>(tenant_id)</code> is the better design.</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2 id="plan-upgrades-and-the-production-readiness-checklist">Plan upgrades and the production readiness checklist</h2>
<h3>Support lifecycle</h3>
<p>PostgreSQL ships a major version about once a year and supports each for five years. PostgreSQL 18 was first released on 2025-09-25, and its final release is due 2030-11-14. PostgreSQL 14 stops receiving fixes on 2026-11-12, so schedule that move now. Since version 10, minors increment the second number (18.5 to 18.6).</p>
<p>&nbsp;</p>
<p>Minor releases carry only bug and security fixes, so they are low-risk to apply, but not zero-risk. Test them in staging and keep a rollback plan. Apply them promptly. Release 18.1 fixed CVE-2025-12817, where <code>CREATE STATISTICS</code> skipped a schema <code>CREATE</code> privilege check.</p>
<p>&nbsp;</p>
<p>Release 18.4 fixed CVE-2026-6575 in <code>pg_restore_attribute_stats</code> along with more than 60 bugs. As of early October 2026 the newest minors are 18.6, 17.11, 16.15, 15.19 and 14.24, all dated 2026-08-13.</p>
<p>&nbsp;</p>
<p>PostgreSQL 19 is in beta (Beta 4 was announced on 2026-09-24). Do not run it in production, but do test your own workloads against it so you find regressions early.</p>
<p>&nbsp;</p>
<h3>Upgrading with pg_upgrade</h3>
<p>PostgreSQL 18 keeps planner statistics across the upgrade, runs checks in parallel with <code>--jobs</code>, and adds <code>--swap</code>, which swaps directories instead of copying, cloning or linking. The animated diagram below walks through this path.</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/postgresql-best-practices-production-diagram-2.gif" alt="Decision path for upgrading PostgreSQL 14: check checksums, run initdb, pg_upgrade --check, --swap, analyze, then a supported major." width="1200" height="675" />
<figcaption>Match the new cluster&#8217;s checksum setting to the old one, dry-run with &#8211;check, then upgrade with &#8211;swap after a verified backup.</figcaption>
</figure>
<p>Four rules govern the new cluster and the run itself:</p>
<p>&nbsp;</p>
<ul>
<li><strong>Compatible encoding and locale.</strong> Initialise the new cluster with an encoding and locale that match the old one. <code>pg_upgrade</code> refuses to proceed if they differ. Read the old values first and pass them to <code>initdb</code> explicitly rather than relying on the environment.</li>
<li><strong>Matching checksum settings.</strong> <code>initdb</code> in PostgreSQL 18 enables data checksums by default, and <code>pg_upgrade</code> requires both clusters to agree. If the old cluster has none, create the new one with <code>--no-data-checksums</code>. You can enable checksums later, offline, with <code>pg_checksums --enable</code> while the cluster is cleanly shut down. It rewrites every data file, so plan the window.</li>
<li><strong>Run as the <code>postgres</code> user.</strong> Run <code>initdb</code> and <code>pg_upgrade</code> as the operating-system user that owns the data directories, normally <code>postgres</code>. Running as root, or as a different user, causes permission failures or leaves files with the wrong owner.</li>
<li><strong><code>--swap</code> leaves the old cluster unusable.</strong> It reuses the old cluster&#8217;s files instead of copying them, so you cannot start the old cluster afterwards. Your rollback is the backup you took before the upgrade, so confirm it restores before you begin.</li>
</ul>
<p>&nbsp;</p>
<p class="code-label"><strong>SQL</strong></p>
<p>&nbsp;</p>
<pre><code class="language-sql">-- Run on the OLD cluster: encoding and locale to reuse for initdb
SELECT datname,
       pg_encoding_to_char(encoding) AS encoding,
       datcollate,
       datctype
FROM pg_database
WHERE datname NOT IN ('template0', 'template1');</code></pre>
<p class="code-label"><strong>Bash/shell</strong></p>
<pre><code class="language-bash"># Run everything as the postgres user
sudo -iu postgres

# Does the old cluster use checksums?
pg_controldata -D /var/lib/postgresql/14/main | grep -i checksum

# New cluster: match encoding and locale from the query above.
# Add --no-data-checksums ONLY if the old cluster has no checksums.
/usr/lib/postgresql/18/bin/initdb \
  --encoding=UTF8 --locale=en_US.UTF-8 \
  --no-data-checksums \
  -D /var/lib/postgresql/18/main

# Dry run: checks only, changes nothing
/usr/lib/postgresql/18/bin/pg_upgrade --check --jobs=4 \
  -b /usr/lib/postgresql/14/bin -B /usr/lib/postgresql/18/bin \
  -d /var/lib/postgresql/14/main -D /var/lib/postgresql/18/main

# Real run, after a verified backup. The old cluster is unusable afterwards.
/usr/lib/postgresql/18/bin/pg_upgrade --jobs=4 --swap \
  -b /usr/lib/postgresql/14/bin -B /usr/lib/postgresql/18/bin \
  -d /var/lib/postgresql/14/main -D /var/lib/postgresql/18/main

# Optional, later, with the cluster cleanly shut down:
# pg_checksums --enable -D /var/lib/postgresql/18/main</code></pre>
<h3> </h3>
<h3> </h3>
<h3>After the upgrade</h3>
<p>&nbsp;</p>
<p>Do not reopen the application to traffic until you have checked three things.</p>
<ol>
<li>Start the new cluster and confirm the version with <code>SELECT version();</code>.</li>
<li>Check statistics. PostgreSQL 18 carries planner statistics across, which shortens the slow period after an upgrade, but verify rather than assume. Find tables that have rows but no statistics, then run <code>ANALYZE</code>.</li>
<li>Run your application&#8217;s test suite and a representative query set against the new cluster. Compare the plans and timings of your slowest queries with a pre-upgrade baseline.</li>
</ol>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">-- Tables that have rows but no planner statistics
SELECT c.oid::regclass AS table_name, c.reltuples::bigint AS est_rows
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
  AND c.reltuples &gt; 0
  AND NOT EXISTS (
    SELECT 1 FROM pg_stats s
    WHERE s.schemaname = n.nspname AND s.tablename = c.relname
  );</code></pre>
<p class="code-label"><strong>Bash/shell</strong></p>
<pre><code class="language-bash"># Refresh statistics in every database
vacuumdb --all --analyze-only --jobs=4</code></pre>
<h3> </h3>
<h3>Low-downtime alternative: logical replication</h3>
<p>If you cannot afford the downtime of <code>pg_upgrade</code>, build a new cluster on the target major version and keep it in sync with logical replication. Then switch applications over in a short window. PostgreSQL 17 improved logical replication for high-availability setups and major-version upgrades, so this path is better supported on recent versions. It also gives you a rollback: the old cluster stays intact until you decide to retire it.</p>
<p>The cost is more moving parts. Logical replication does not copy schema changes, so you must create the schema on the target and freeze DDL during the migration.</p>
<p>Sequence values need to be synchronised before cutover.</p>
<p>Tables need suitable replica identities, usually a primary key. Rehearse the whole procedure on a copy of production first.</p>
<h3> </h3>
<h3>Production readiness checklist</h3>
<p>&nbsp;</p>
<p>Each item names something you can check. If you cannot show the evidence, treat the item as not done.</p>
<ol>
<li><strong>Latest minor applied.</strong> <code>SHOW server_version;</code> matches the newest minor of your major (18.6, 17.11, 16.15, 15.19 or 14.24 as of this writing).</li>
<li><strong>Supported major with a dated plan.</strong> You are not on a version past its end-of-fix date. If you run PostgreSQL 14, the upgrade must finish before 2026-11-12.</li>
<li><strong>WAL archiving works.</strong> <code>archive_mode = on</code> with a working archive command or library. <code>pg_stat_archiver</code> shows a recent <code>last_archived_time</code> and a <code>failed_count</code> that is not growing. Alert when archiving fails, because unarchived WAL piles up on the primary&#8217;s disk.</li>
<li><strong>A restore has been tested.</strong> You have restored a base backup plus archived WAL to a separate host, reached a chosen point in time, and run application queries against it. Record the date and how long it took, and repeat on a schedule.</li>
<li><strong><code>pg_stat_statements</code> is enabled.</strong> It is in <code>shared_preload_libraries</code> (this needs a restart), and <code>CREATE EXTENSION pg_stat_statements;</code> has been run in the database you query. A dashboard or a regular review uses it to find the slowest and most frequent statements.</li>
<li><strong>Autovacuum is monitored.</strong> You alert on dead tuples and time since the last autovacuum for large tables, on long-running transactions and stale replication slots that hold back cleanup, and on transaction ID age (<code>age(datfrozenxid)</code>) approaching the freeze limits.</li>
<li><strong>TLS is required.</strong> <code>ssl = on</code> with a valid certificate, and <code>pg_hba.conf</code> uses <code>hostssl</code> entries rather than plain <code>host</code> for network clients. Clients connect with <code>sslmode=verify-full</code> where possible.</li>
<li><strong>Passwords use <code>scram-sha-256</code>.</strong> <code>password_encryption = scram-sha-256</code>, and no line in <code>pg_hba.conf</code> uses <code>md5</code> or <code>trust</code> for network access. Existing <code>md5</code> hashes cannot be converted, so reset those passwords after changing the setting.</li>
<li><strong>Least-privilege roles.</strong> Applications do not connect as a superuser or as the schema owner. Check with <code>\du</code> and by reviewing grants.</li>
<li><strong>Replication lag has thresholds.</strong> Derive them from your recovery point objective: one warning level and one paging level, measured on <code>pg_stat_replication</code> (<code>replay_lag</code> or byte lag) and on standbys. Alert also when a standby disconnects entirely, since a missing row looks like zero lag.</li>
<li><strong>Failover has been exercised.</strong> You have promoted a standby on purpose and confirmed that applications reconnect.</li>
<li><strong>Data checksums are on, or the plan is written down.</strong> New PostgreSQL 18 clusters have them by default. For older clusters, schedule an offline <code>pg_checksums --enable</code>.</li>
<li><strong>Disk and connection headroom are alerted.</strong> You get a warning before the data, WAL or archive volumes fill, and before connection counts approach <code>max_connections</code>.</li>
</ol>
<p class="code-label"><strong>Text</strong></p>
<pre><code class="language-text"># pg_hba.conf: TLS required, SCRAM only, no trust/md5 for network clients
# TYPE     DATABASE  USER  ADDRESS        METHOD
local      all       postgres              peer
hostssl    all       all   10.0.0.0/16    scram-sha-256
hostnossl  all       all   0.0.0.0/0      reject</code></pre>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">-- WAL archiving: failures should be zero and last_archived_time recent
SELECT archived_count, failed_count, last_archived_time, last_failed_time
FROM pg_stat_archiver;

-- Replication lag per standby (run on the primary)
SELECT application_name, state, replay_lag,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes
FROM pg_stat_replication;

-- Autovacuum: tables with the most dead tuples
SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

-- Transaction ID age per database
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;

-- Auth and TLS settings
SHOW password_encryption;
SHOW ssl;</code></pre>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li><strong>Patch minors promptly.</strong> Run the latest minor of your major version. Minors carry only bug and security fixes, so they are low-risk, but still roll them out through staging first.</li>
<li><strong>Retire PostgreSQL 14 now.</strong> Set an upgrade date before its final fixes arrive in November 2026, and prefer PostgreSQL 18 as the target.</li>
<li><strong>Use <code>uuidv7()</code> for UUID primary keys.</strong> Timestamp-ordered values keep inserts localized in the index.</li>
<li><strong>Recheck composite index column order.</strong> Skip scan in PostgreSQL 18 may make some indexes redundant, so confirm with query plans.</li>
<li><strong>Enforce integrity in the database.</strong> Use temporal constraints instead of application-side overlap checks, and keep data checksums enabled.</li>
<li><strong>Rehearse <code>pg_upgrade</code>.</strong> Run <code>pg_upgrade --check</code> first and take a backup before using <code>--swap</code>.</li>
<li><strong>Prefer OAuth over shared static passwords, and benchmark <code>io_method</code> yourself.</strong> Inspect <code>pg_aios</code> on your own storage before changing the default.</li>
<li><strong>Keep PostgreSQL 19 beta out of production.</strong> Use it in test environments to try your own workloads.</li>
</ul>
<h2 id="faq">FAQ</h2>
<h3>Should I upgrade to PostgreSQL 18 or 17?</h3>
<p>Choose 18 unless something concrete blocks it, such as an extension, driver or managed service that does not support it yet. PostgreSQL 18 was first released on 2025-09-25 and is scheduled for final fixes on 2030-11-14, so it gives you the longest support window. Upgrading to 17 means you will have to run another major upgrade sooner.</p>
<p>PostgreSQL 18 also makes the upgrade itself easier:</p>
<ul>
<li>Planner statistics carry over, so the new cluster reaches normal performance sooner.</li>
<li><code>pg_upgrade</code> runs its checks in parallel with <code>--jobs</code>.</li>
<li>The new <code>--swap</code> flag swaps directories instead of copying, cloning or linking files, which helps most when a database has many objects.</li>
</ul>
<p>PostgreSQL 17 is a reasonable stop if your dependencies force it. It reworked vacuum memory management, improved high-concurrency workloads and bulk loading, and improved logical replication for high availability and major upgrades. Do not pick 16 or 15 as a target for a new upgrade. They have less support time left and you would repeat the work soon.</p>
<p>Either way, restore a production backup into a staging cluster, run <code>pg_upgrade</code> there, and replay your real workload before you commit. If you are still on 14, its last fixes are due on 2026-11-12, so schedule this now.</p>
<p>&nbsp;</p>
<h3>Can I enable data checksums on an existing cluster?</h3>
<p>Yes, but not online. <code>initdb</code> in PostgreSQL 18 enables checksums by default, which does nothing for clusters that already exist. First check your current state:</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">SHOW data_checksums;<br /></code></pre>
<p>If it returns <code>off</code>, the usual route is the <code>pg_checksums</code> tool. It requires a cleanly shut-down cluster and rewrites every data file to add checksums, so downtime grows with database size. Run it on a restored copy first to measure that time. If you cannot afford the downtime, build a new cluster with checksums enabled and move the data over with logical replication.</p>
<p>The setting matters for major upgrades because <code>pg_upgrade</code> requires the old and new clusters to match. If your old cluster has no checksums and you do not want to change that during the upgrade, initialize the new 18 cluster without them:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p class="code-label"><strong>Bash</strong></p>
<pre><code class="language-bash">initdb --no-data-checksums -D /var/lib/postgresql/18/main
pg_upgrade \
  --old-bindir=/usr/lib/postgresql/16/bin \
  --new-bindir=/usr/lib/postgresql/18/bin \
  --old-datadir=/var/lib/postgresql/16/main \
  --new-datadir=/var/lib/postgresql/18/main \
  --jobs=4 --check</code></pre>
<p>Dropping <code>--check</code> runs the real upgrade. I recommend treating the upgrade and the checksum change as two separate maintenance steps. If something goes wrong, you then know which change caused it. The trade-off is that checksums add a small CPU cost, but they let PostgreSQL detect storage-level corruption instead of silently reading bad pages.</p>
<h3>Should I use uuidv7() on existing tables?</h3>
<p>Use it as the default for new rows and leave existing keys alone. <code>uuidv7()</code> is new in PostgreSQL 18 and generates timestamp-ordered UUIDs. New values are inserted near the end of a primary key index, which suits indexes and caching better than random UUIDs. Changing the default does not rewrite or invalidate existing rows, because both kinds of value are ordinary <code>uuid</code> values:</p>
<p class="code-label"><strong>SQL</strong></p>
<pre><code class="language-sql">ALTER TABLE orders
  ALTER COLUMN id SET DEFAULT uuidv7();</code></pre>
<p>Do not rewrite existing primary keys just to get ordering. That would mean updating every foreign key and external reference, and the benefit applies only to new inserts. Two caveats apply:</p>
<ul>
<li>A UUIDv7 embeds its creation time, so do not use it where exposing creation time to clients is a problem.</li>
<li>The function does not exist before PostgreSQL 18. If any replica or application environment still runs an older major, applying the default there will fail.</li>
</ul>
<p>Apply the change after every node runs 18. <code>uuidv4()</code> is also new in 18 as an alias for <code>gen_random_uuid()</code>, if you want a name that states the version.</p>
<h3>Do multicolumn indexes need to be redesigned for PostgreSQL 18?</h3>
<p>Not immediately. PostgreSQL 18 adds skip scan on multicolumn B-tree indexes, so a query that omits an equality condition on one or more leading columns can still use the index. That can make some single-purpose indexes redundant, and it relaxes how strictly you must order columns in new composite indexes.</p>
<p>Do not drop indexes on this basis alone. Compare the plans and timings of your real queries after the upgrade, and only then retire an index you can show is unused or covered. Dropping an index in a hurry is harder to undo than keeping it.</p>
<p>&nbsp;</p>
<h3>Is it safe to apply PostgreSQL minor releases without a long test cycle?</h3>
<p>Minor releases carry only bug and security fixes, which makes them low-risk, but not risk-free. Skipping them has a real cost: 18.1 fixed CVE-2025-12817, where <code>CREATE STATISTICS</code> skipped a schema <code>CREATE</code> privilege check, and 18.4 fixed CVE-2026-6575 in <code>pg_restore_attribute_stats</code> along with over 60 bugs.</p>
<p>Roll the update through staging first and confirm you have a backup you have actually restored. Then move production to the latest minor of your major: 18.6, 17.11, 16.15, 15.19 or 14.24. Since version 10, minor releases change the second number (for example 10.0 to 10.1), so 18.5 to 18.6 is a minor update.</p>
<h3>Should I test PostgreSQL 19 now?</h3>
<p>Yes, run your own workload against it in a non-production environment. Beta 4 was announced on 2026-09-24, and the project encourages this kind of testing, so problems can be reported before the final release. Do not run a beta in production. For production, use PostgreSQL 18.</p>
<h2 id="sources">Sources and further reading</h2>
<ul>
<li><a href="https://www.postgresql.org/" target="_blank" rel="noopener">PostgreSQL: The world&#8217;s most advanced open source database</a> (PostgreSQL Global Development Group)</li>
<li><a href="https://www.postgresql.org/support/versioning/" target="_blank" rel="noopener">PostgreSQL: Versioning Policy</a> (PostgreSQL Global Development Group)</li>
<li><a href="https://www.postgresql.org/about/news/postgresql-18-released-3142/" target="_blank" rel="noopener">PostgreSQL: PostgreSQL 18 Released!</a> (PostgreSQL Global Development Group)</li>
<li><a href="https://www.postgresql.org/docs/release/18.0/" target="_blank" rel="noopener">PostgreSQL 18.0 Release Notes</a> (PostgreSQL Global Development Group)</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 class="wp-block-paragraph"></p><p>The post <a href="https://thecodebuzz.com/postgresql-best-practices-production/">PostgreSQL – Best Practices for Production</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/postgresql-best-practices-production/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Monolith First: Why Startups Should Skip Microservices</title>
		<link>https://thecodebuzz.com/monolith-first-startups-skip-microservices/</link>
					<comments>https://thecodebuzz.com/monolith-first-startups-skip-microservices/#respond</comments>
		
		<dc:creator><![CDATA[Izzy]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 02:55:53 +0000</pubDate>
				<category><![CDATA[Startups]]></category>
		<category><![CDATA[.net]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[microservices]]></category>
		<category><![CDATA[modular monolith]]></category>
		<category><![CDATA[startups]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30705</guid>

					<description><![CDATA[<p>Learn why most startups should build a modular monolith first, how to structure it in C#/.NET, and when to extract a service. Includes code and a checklist.</p>
<p>The post <a href="https://thecodebuzz.com/monolith-first-startups-skip-microservices/">Monolith First: Why Startups Should Skip Microservices</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Fowler&#8217;s 2015 &#8220;MonolithFirst&#8221; note observed that almost every successful microservice system he knew of began as a monolith that grew too big, and that most systems built as microservices from scratch ran into serious trouble.</p>
<p>&nbsp;</p>
<p>That is anecdote, not measured data. It still points to a lower-risk start: one deployable unit with strict internal modules. This article shows how to build one in .NET, how to keep its boundaries honest, and when to extract a service.</p>
<p>&nbsp;</p>
<h2>Why start with a monolith</h2>
<ul>
<li><strong>Unstable boundaries:</strong> a new domain keeps changing. Early splits create arbitrary seams in a distributed system.</li>
<li><strong>Operational load:</strong> microservices add service discovery, tracing, inter-service auth, gateways and orchestration. A monolith has one pipeline, one log destination and one process to debug.</li>
<li> </li>
</ul>
<table>
<thead>
<tr>
<th> </th>
<th>Monolith</th>
<th>Modular monolith</th>
<th>Microservices</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deployables</td>
<td>1</td>
<td>1</td>
<td>Many</td>
</tr>
<tr>
<td>Debugging</td>
<td>One process</td>
<td>One process</td>
<td>Cross-network, needs tracing</td>
</tr>
<tr>
<td>Consistency</td>
<td>One transaction possible</td>
<td>Transactions inside a module; across modules, an outbox and eventual consistency</td>
<td>Eventual consistency</td>
</tr>
<tr>
<td>Moving boundaries</td>
<td>Cheap, easy to tangle</td>
<td>Cheap</td>
<td>Expensive</td>
</tr>
</tbody>
</table>
<p>One 2026 vendor guide suggests a modular monolith under about 15 developers and microservices at 50 or more.</p>
<p>Those are heuristics, not rules. A 2014 article, &#8220;Don&#8217;t start with a monolith,&#8221; argued that a team that can&#8217;t build a well-structured monolith can&#8217;t build well-structured services. The real risk is poor modularity.</p>
<figure class="wp-block-image size-large"><img decoding="async" src="https://thecodebuzz.com/wp-content/uploads/2026/10/monolith-first-startups-skip-microservices-2.jpg" alt="engineer, engineering, structural engineer" />
<figcaption>Image by <a href="https://pixabay.com/users/This_is_Engineering-11384528/" target="_blank" rel="noopener nofollow">This_is_Engineering</a> from <a href="https://pixabay.com/photos/engineer-engineering-4925116/" target="_blank" rel="noopener nofollow">Pixabay</a></figcaption>
</figure>
<h2>Build a modular monolith in .NET</h2>
<p>Requires .NET 8+ (C# 12 primary constructors). Packages: <code>Microsoft.EntityFrameworkCore.SqlServer</code>, <code>Microsoft.Extensions.Http.Resilience</code>, xUnit. Each class library needs usings for <code>Microsoft.EntityFrameworkCore</code>, <code>Microsoft.Extensions.DependencyInjection</code>, <code>Microsoft.Extensions.Hosting</code>, <code>Microsoft.Extensions.Logging</code>, <code>System.Text.Json</code> and <code>System.Net.Http.Json</code> as needed.</p>
<p>&nbsp;</p>
<pre><code class="language-text">src/
  Host/               # minimal API; references modules and contracts
  SharedKernel/       # IDomainEvent, IEventHandler, event bus
  Orders.Contracts/   # IOrdersApi, DTOs, OrderPlaced (public)
  Orders/             # internal DbContext, logic
  Billing/            # references Orders.Contracts only
tests/ArchitectureTests/</code></pre>
<p>Modules talk through contracts or events.</p>
<p>Each module has its own DbContext and schema, so there is no automatic transaction across modules.</p>
<p>To avoid losing events, Orders writes the event to an outbox table in the same <code>SaveChanges</code> as the order.</p>
<p>A background dispatcher publishes it afterwards.</p>
<p>Delivery is at-least-once, so handlers must be idempotent. A broker will give you the same contract later.</p>
<p>&nbsp;</p>
<pre><code class="language-csharp">// SharedKernel
public interface IDomainEvent { }
public interface IEventHandler&lt;in T&gt; where T : IDomainEvent
{
    Task HandleAsync(T e, CancellationToken ct);
}
public sealed class InProcessEventBus(IServiceProvider sp)
{
    public async Task PublishAsync&lt;T&gt;(T e, CancellationToken ct) where T : IDomainEvent
    {
        foreach (var h in sp.GetServices&lt;IEventHandler&lt;T&gt;&gt;())
            await h.HandleAsync(e, ct);
    }
}

// Orders.Contracts
public sealed record OrderDto(Guid Id, decimal Total, string Status);
public sealed record OrderPlaced(Guid OrderId, decimal Total) : IDomainEvent;
public interface IOrdersApi
{
    Task&lt;OrderDto?&gt; GetOrderAsync(Guid id, CancellationToken ct);
    Task&lt;Guid&gt; PlaceOrderAsync(decimal total, CancellationToken ct);
}

// Orders (internal)
internal sealed class OrderEntity
{
    public Guid Id { get; set; }
    public decimal Total { get; set; }
    public string Status { get; set; } = "Placed";
}
internal sealed class OutboxMessage
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public string Payload { get; set; } = "";
    public DateTime? ProcessedAt { get; set; }
}
internal sealed class OrdersDbContext(DbContextOptions&lt;OrdersDbContext&gt; options) : DbContext(options)
{
    public DbSet&lt;OrderEntity&gt; Orders =&gt; Set&lt;OrderEntity&gt;();
    public DbSet&lt;OutboxMessage&gt; Outbox =&gt; Set&lt;OutboxMessage&gt;();
    protected override void OnModelCreating(ModelBuilder b) =&gt; b.HasDefaultSchema("orders");
}
internal sealed class OrdersApi(OrdersDbContext db) : IOrdersApi
{
    public Task&lt;OrderDto?&gt; GetOrderAsync(Guid id, CancellationToken ct) =&gt;
        db.Orders.AsNoTracking().Where(o =&gt; o.Id == id)
            .Select(o =&gt; new OrderDto(o.Id, o.Total, o.Status))
            .FirstOrDefaultAsync(ct);

    public async Task&lt;Guid&gt; PlaceOrderAsync(decimal total, CancellationToken ct)
    {
        var order = new OrderEntity { Id = Guid.NewGuid(), Total = total };
        db.Orders.Add(order);
        db.Outbox.Add(new OutboxMessage
        {
            Id = Guid.NewGuid(),
            Payload = JsonSerializer.Serialize(new OrderPlaced(order.Id, total))
        });
        await db.SaveChangesAsync(ct); // order and event commit together
        return order.Id;
    }
}
internal sealed class OutboxDispatcher(IServiceScopeFactory scopes, ILogger&lt;OutboxDispatcher&gt; log) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
        while (await timer.WaitForNextTickAsync(ct))
        {
            using var scope = scopes.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService&lt;OrdersDbContext&gt;();
            var bus = scope.ServiceProvider.GetRequiredService&lt;InProcessEventBus&gt;();
            var pending = await db.Outbox.Where(m =&gt; m.ProcessedAt == null)
                .OrderBy(m =&gt; m.CreatedAt).Take(20).ToListAsync(ct);
            foreach (var m in pending)
            {
                try
                {
                    await bus.PublishAsync(JsonSerializer.Deserialize&lt;OrderPlaced&gt;(m.Payload)!, ct);
                    m.ProcessedAt = DateTime.UtcNow;
                    await db.SaveChangesAsync(ct);
                }
                catch (Exception ex) when (ex is not OperationCanceledException)
                {
                    log.LogError(ex, "Outbox message {Id} failed; will retry", m.Id);
                }
            }
        }
    }
}
public static class OrdersModule
{
    public static IServiceCollection AddOrdersModule(this IServiceCollection s, string cs)
    {
        s.AddDbContext&lt;OrdersDbContext&gt;(o =&gt;
            o.UseSqlServer(cs, sql =&gt; sql.MigrationsHistoryTable("__EFMigrationsHistory", "orders")));
        s.AddScoped&lt;IOrdersApi, OrdersApi&gt;();
        s.AddHostedService&lt;OutboxDispatcher&gt;();
        return s;
    }
}</code></pre>
<p>Billing reacts to <code>OrderPlaced</code> and sees only <code>Orders.Contracts</code>:</p>
<pre><code class="language-csharp">// Billing (internal; references Orders.Contracts and SharedKernel)
internal sealed class Invoice { public Guid OrderId { get; set; } public decimal Amount { get; set; } }
internal sealed class BillingDbContext(DbContextOptions&lt;BillingDbContext&gt; options) : DbContext(options)
{
    public DbSet&lt;Invoice&gt; Invoices =&gt; Set&lt;Invoice&gt;();
    protected override void OnModelCreating(ModelBuilder b)
    {
        b.HasDefaultSchema("billing");
        b.Entity&lt;Invoice&gt;().HasKey(i =&gt; i.OrderId);
    }
}
internal sealed class CreateInvoiceOnOrderPlaced(BillingDbContext db) : IEventHandler&lt;OrderPlaced&gt;
{
    public async Task HandleAsync(OrderPlaced e, CancellationToken ct)
    {
        if (await db.Invoices.AnyAsync(i =&gt; i.OrderId == e.OrderId, ct)) return; // idempotent
        db.Invoices.Add(new Invoice { OrderId = e.OrderId, Amount = e.Total });
        await db.SaveChangesAsync(ct);
    }
}
public static class BillingModule
{
    public static IServiceCollection AddBillingModule(this IServiceCollection s, string cs)
    {
        s.AddDbContext&lt;BillingDbContext&gt;(o =&gt;
            o.UseSqlServer(cs, sql =&gt; sql.MigrationsHistoryTable("__EFMigrationsHistory", "billing")));
        s.AddScoped&lt;IEventHandler&lt;OrderPlaced&gt;, CreateInvoiceOnOrderPlaced&gt;();
        return s;
    }
}</code></pre>
<p>Wire it up in <code>Host/Program.cs</code>. Create migrations for each DbContext before running.</p>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped&lt;InProcessEventBus&gt;();
builder.Services
    .AddOrdersModule(builder.Configuration.GetConnectionString("Orders")!)
    .AddBillingModule(builder.Configuration.GetConnectionString("Billing")!);

var app = builder.Build();
app.MapGet("/orders/{id:guid}", async (Guid id, IOrdersApi api, CancellationToken ct) =&gt;
    await api.GetOrderAsync(id, ct) is { } o ? Results.Ok(o) : Results.NotFound());
app.MapPost("/orders", async (PlaceOrderRequest req, IOrdersApi api, CancellationToken ct) =&gt;
    Results.Ok(await api.PlaceOrderAsync(req.Total, ct)));
app.Run();

record PlaceOrderRequest(decimal Total);</code></pre>
<h2>Where boundaries really leak</h2>
<p>Billing&#8217;s types are internal and in another project, so the compiler already blocks direct access to them. The real leaks are raw SQL against another schema, a shared DbContext, and a stray project reference or <code>InternalsVisibleTo</code>.</p>
<p>Block the SQL leak in the database. Give each module its own login, use one connection string per module, and run migrations with a separate admin account:</p>
<pre><code class="language-sql">CREATE LOGIN orders_app WITH PASSWORD = '&lt;secret&gt;';
CREATE USER orders_app FOR LOGIN orders_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::orders TO orders_app;</code></pre>
<p>Catch the reference leak in CI. The test project must reference every module assembly:</p>
<pre><code class="language-csharp">public class ModuleBoundaryTests
{
    static readonly Assembly[] Modules =
        [typeof(OrdersModule).Assembly, typeof(BillingModule).Assembly];

    [Fact]
    public void Module_assemblies_were_found()
    {
        // Guards against a vacuous pass if a module is renamed
        Assert.Equal(["Billing", "Orders"], Modules.Select(m =&gt; m.GetName().Name!).Order());
    }

    [Fact]
    public void Modules_do_not_reference_each_other()
    {
        var names = Modules.Select(m =&gt; m.GetName().Name!).ToHashSet();
        foreach (var module in Modules)
        {
            var leaked = module.GetReferencedAssemblies()
                .Select(a =&gt; a.Name!).Where(n =&gt; names.Contains(n) &amp;&amp; n != module.GetName().Name);
            Assert.Empty(leaked);
        }
    }
}</code></pre>
<p>This covers every module pair and passes only if they depend on each other&#8217;s <code>.Contracts</code> assemblies alone. Add a module to the array when you create it.</p>
<h2>When to extract a service</h2>
<p>Valid reasons are concrete: independent scaling, separate teams, different deployment cadences, a different tech stack, compliance isolation, or fault isolation with circuit breakers. Trends and résumé-driven design are not on the list. Before extracting, check that:</p>
<ul>
<li>the boundary has stayed stable for months,</li>
<li>only this module owns its data, with no cross-module joins,</li>
<li>you have measured the bottleneck,</li>
<li>you have capacity for tracing, auth, deployment and on-call.</li>
</ul>
<p>Then pick the cleanest module, put an HTTP client behind the existing interface, move its schema to its own database, and keep a rollback path.</p>
<pre><code class="language-csharp">internal sealed class HttpOrdersApi(HttpClient http) : IOrdersApi
{
    public async Task&lt;OrderDto?&gt; GetOrderAsync(Guid id, CancellationToken ct)
    {
        using var response = await http.GetAsync($"orders/{id}", ct);
        if (response.StatusCode == HttpStatusCode.NotFound) return null;
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync&lt;OrderDto&gt;(ct);
    }

    public async Task&lt;Guid&gt; PlaceOrderAsync(decimal total, CancellationToken ct)
    {
        using var response = await http.PostAsJsonAsync("orders", new { total }, ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync&lt;Guid&gt;(ct);
    }
}

public static class OrdersClientExtensions
{
    public static IServiceCollection AddOrdersHttpClient(this IServiceCollection s, Uri baseAddress)
    {
        s.AddHttpClient&lt;IOrdersApi, HttpOrdersApi&gt;(c =&gt; c.BaseAddress = baseAddress)
         .AddStandardResilienceHandler(o =&gt; o.Retry.DisableForUnsafeHttpMethods());
        return s;
    }
}</code></pre>
<p>The standard handler adds timeouts, retries and a circuit breaker. Disabling retries for POST avoids duplicate orders. Decide how callers see an open circuit or a 5xx.</p>
<p>&nbsp;</p>
<p>The outbox dispatcher then has to publish to a broker instead of the in-process bus. Hosting prices and limits change often, so check the provider&#8217;s current documentation.</p>
<p>&nbsp;</p>
<h2>Key takeaways</h2>
<ul>
<li>Start with one deployable unit and optimize for learning speed.</li>
<li>Modularity matters more than the deployment model. Enforce it with contracts, per-schema DB permissions and tests.</li>
<li>Cross-module consistency is not free. Use an outbox and idempotent handlers.</li>
<li>Extract only for reasons you can name or measure.</li>
</ul>
<h2>FAQ</h2>
<h3>Can I use a single database and still call it modular?</h3>
<p>Yes, if each module owns its schema and nothing else touches it. Enforce that with per-module database permissions, not just convention.</p>
<p>&nbsp;</p>
<h3>Is a modular monolith overkill for a two-person team?</h3>
<p>&nbsp;</p>
<p>This is judgement, not evidence. While you are still validating the idea, a plain monolith is fine. Contracts projects and one schema per module cost little, though, and make a later split easier.</p>
<p>&nbsp;</p>
<h2>Sources and further reading</h2>
<ul>
<li><a href="https://www.goodreads.com/author/show/25215.Martin_Fowler/blog?page=34" target="_blank" rel="noopener">Martin Fowler&#8217;s Blog, page 34 (includes the MonolithFirst bliki entry, June 3, 2015)</a> (Goodreads (mirror of Martin Fowler&#8217;s blog))</li>
<li><a href="https://sdtimes.com/continuous-integration/martin-fowler-monolithic-apps-first-microservices-later/" target="_blank" rel="noopener">Martin Fowler: Monolithic apps first, microservices later &#8211; SD Times</a> (SD Times)</li>
<li><a href="https://academy.threedots.tech/knowledge/monolith-first/" target="_blank" rel="noopener">Monolith First</a> (Three Dots Labs Academy)</li>
<li><a href="https://technijian.com/software-development/microservices-vs-monolith-for-startups-the-honest-2026-decision-guide/" target="_blank" rel="noopener">Microservices vs Monolith for Startups: 2026 Architecture Guide</a> (Technijian)</li>
<li><a href="https://distantjob.com/blog/monolith-vs-microservices/" target="_blank" rel="noopener">Monolith vs Microservices in 2026: The Decision Framework Your Engineering Team Needs</a> (DistantJob)</li>
<li><a href="https://martinfowler.com/tags/microservices.html" target="_blank" rel="noopener">tagged by: microservices</a> (martinfowler.com)</li>
</ul>
<p class="image-credits"><em>Images: Image by <a href="https://pixabay.com/users/Innovalabs-22783312/" target="_blank" rel="noopener nofollow">Innovalabs</a> from <a href="https://pixabay.com/photos/software-developer-web-developer-6521720/" target="_blank" rel="noopener nofollow">Pixabay</a>; Image by <a href="https://pixabay.com/users/This_is_Engineering-11384528/" target="_blank" rel="noopener nofollow">This_is_Engineering</a> from <a href="https://pixabay.com/photos/engineer-engineering-4925116/" target="_blank" rel="noopener nofollow">Pixabay</a>.</em></p>


<p class="wp-block-paragraph"></p><p>The post <a href="https://thecodebuzz.com/monolith-first-startups-skip-microservices/">Monolith First: Why Startups Should Skip Microservices</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/monolith-first-startups-skip-microservices/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Best French Learning Youtube video series</title>
		<link>https://thecodebuzz.com/best-french-learning-youtube-video-series/</link>
					<comments>https://thecodebuzz.com/best-french-learning-youtube-video-series/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 18 Sep 2026 02:46:11 +0000</pubDate>
				<category><![CDATA[TechBuzz]]></category>
		<guid isPermaLink="false">https://thecodebuzz.com/?p=30693</guid>

					<description><![CDATA[<p>Best French Learning Youtube video series Omar&#8217;s First Day — Le Passé Composé &#8211; Episode 2 Omar arrive — Le présent Épisode 1 French Questions &#8211; Omar Asks for Help in french &#8211; Episode 5</p>
<p>The post <a href="https://thecodebuzz.com/best-french-learning-youtube-video-series/">Best French Learning Youtube video series</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Best French Learning Youtube video series  </p>



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



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



<p class="wp-block-paragraph"><a href="https://studio.youtube.com/video/SXgCKLAr-Mc/analytics/tab-overview/period-default">Omar&#8217;s First Day — Le Passé Composé &#8211; Episode 2</a></p>



<p class="wp-block-paragraph"><a href="https://studio.youtube.com/video/Dqd2_pQ2_a0/analytics/tab-overview/period-default">Omar arrive — Le présent Épisode 1</a></p>



<p class="wp-block-paragraph"><a href="https://studio.youtube.com/video/eG3nZWUlfHs/edit">French Questions &#8211; Omar Asks for Help in french &#8211; Episode 5</a></p>



<p class="wp-block-paragraph"></p><p>The post <a href="https://thecodebuzz.com/best-french-learning-youtube-video-series/">Best French Learning Youtube video series</a> first appeared on <a href="https://thecodebuzz.com">TheCodeBuzz</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://thecodebuzz.com/best-french-learning-youtube-video-series/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
